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

Dependencies:   FastIO FastPWM SimpleDMA mbed

Fork of Pinscape_Controller by Mike R

Files at this revision

API Documentation at this revision

Comitter:
mjr
Date:
Wed Oct 21 21:53:07 2015 +0000
Parent:
32:cbff13b98441
Child:
34:6b981a2afab7
Commit message:
With expansion board 5940 "power enable" output; saving this feature, which is to be removed.

Changed in this revision

TLC5940/TLC5940.h 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
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
--- a/TLC5940/TLC5940.h	Sat Sep 26 02:15:59 2015 +0000
+++ b/TLC5940/TLC5940.h	Wed Oct 21 21:53:07 2015 +0000
@@ -20,6 +20,50 @@
 #ifndef TLC5940_H
 #define TLC5940_H
 
+// Should we do the grayscale update within the blanking interval?
+// If this is set to 1, we'll send grayscale data during the blanking
+// interval; if 0, we'll send grayscale during the PWM cycle.
+// Mode 0 is the *intended* way of using these chips, but mode 1
+// produces a more stable signal in my test setup.
+//
+// In my breadboard testing, using the standard data-during-PWM
+// mode causes some amount of signal instability with multiple
+// daisy-chained TLC5940's.  It appears that there's some signal
+// interference (maybe RF or electrical ringing in the wires) that
+// can make the bit data and/or clock prone to noise that causes
+// random bits to propagate down the daisy chain.  This happens
+// frequently enough in my breadboard setup to be visible as
+// regular flicker.  Careful wiring, short wire runs, and decoupling
+// capacitors noticeably improve it, but I haven't been able to 
+// eliminate it entirely in my test setup.  Using the data-during-
+// blanking mode, however, *does* eliminate it entirely.
+//
+// It clearly should be possible to eliminate the signal problems
+// in a well-designed PCB layout, but for the time being, I'm
+// making data-during-blanking the default, since it provides
+// such a noticeable improvement in my test setup, and the cost
+// is minimal.  The cost is that it lengthens the blanking interval
+// slightly.  With four chips and the SPI clock at 28MHz, the 
+// full data update takes 27us; with the PWM clock at 500kHz, the 
+// grayscale cycle is 8192us.  This means that the 27us data send 
+// keeps the BLANK asserted for an additional 0.3% of the cycle 
+// time, which in term reduces output brightness by the same amount.
+// This brightness reduction isn't noticeable on its own, but it
+// can be seen as a flicker on data cycles if we send data on
+// some blanking cycles but not on others.  To eliminate the
+// flicker, the code sends a data update on *every* cycle when
+// using this mode to ensure that the 0.3% brightness reduction
+// is uniform across time.
+//
+// When using this code with TLC5940 chips on a PCB, I recommend
+// doing a test: set this to 0, run the board, turn on all outputs
+// (connected to LEDs), and observe the results.  If you don't
+// see any randomness or flicker in a minute or two of observation,
+// you're getting a good clean signal throughout the daisy chain
+// and don't need the workaround.  If you do see any instability, 
+// set this back to 1.
+#define DATA_UPDATE_INSIDE_BLANKING  1
+
 #include "mbed.h"
 #include "FastPWM.h"
 #include "SimpleDMA.h"
@@ -27,10 +71,16 @@
 /**
   * SPI speed used by the mbed to communicate with the TLC5940
   * The TLC5940 supports up to 30Mhz.  It's best to keep this as
-  * high as the microcontroller will allow, since a higher SPI 
-  * speed yields a faster grayscale data update.  However, if
-  * you have problems with unreliable signal transmission to the
-  * TLC5940s, reducing this speed might help.
+  * high as possible, since a higher SPI speed yields a faster 
+  * grayscale data update.  However, I've seen some slight
+  * instability in the signal in my breadboard setup using the
+  * full 30MHz, so I've reduced this slightly, which seems to
+  * yield a solid signal.  The limit will vary according to how
+  * clean the signal path is to the chips; you can probably crank
+  * this up to full speed if you have a well-designed PCB, good
+  * decoupling capacitors near the 5940 VCC/GND pins, and short
+  * wires between the KL25Z and the PCB.  A short, clean path to
+  * KL25Z ground seems especially important.
   *
   * The SPI clock must be fast enough that the data transmission
   * time for a full update is comfortably less than the blanking 
@@ -49,7 +99,7 @@
   * isn't a factor.  E.g., at SPI=30MHz and GSCLK=500kHz, 
   * t(blank) is 8192us and t(refresh) is 25us.
   */
-#define SPI_SPEED 3000000
+#define SPI_SPEED 2800000
 
 /**
   * The rate at which the GSCLK pin is pulsed.   This also controls 
@@ -116,15 +166,19 @@
           gsclk(GSCLK),
           blank(BLANK),
           xlat(XLAT),
-          nchips(nchips),
-          newGSData(true)
+          nchips(nchips)
     {
-        // Set initial output pin states - XLAT off, BLANK on (BLANK turns off
-        // all of the outputs while we're setting up)
+        // set XLAT to initially off
         xlat = 0;
+        
+        // Assert BLANK while starting up, to keep the outputs turned off until
+        // everything is stable.  This helps prevent spurious flashes during startup.
+        // (That's not particularly important for lights, but it matters more for
+        // tactile devices.  It's a bit alarming to fire a replay knocker on every
+        // power-on, for example.)
         blank = 1;
         
-        // allocate the grayscale buffer
+        // allocate the grayscale buffer, and set all outputs to fully off
         gs = new unsigned short[nchips*16];
         memset(gs, 0, nchips*16*sizeof(gs[0]));
         
@@ -137,6 +191,22 @@
         // format 0.
         spi.format(8, 0);
         spi.frequency(SPI_SPEED);
+        
+        // Send out a full data set to the chips, to clear out any random
+        // startup data from the registers.  Include some extra bits - there
+        // are some cases (such as after sending dot correct commands) where
+        // an extra bit per chip is required, and the initial state is 
+        // somewhat unpredictable, so send extra just to make sure we cover
+        // all bases.  This does no harm; extra bits just fall off the end of
+        // the daisy chain, and since we want all registers set to 0, we can
+        // send arbitrarily many extra 0's.
+        for (int i = 0 ; i < nchips*25 ; ++i)
+            spi.write(0);
+            
+        // do an initial XLAT to latch all of these "0" values into the
+        // grayscale registers
+        xlat = 1;
+        xlat = 0;
 
         // Allocate a DMA buffer.  The transfer on each cycle is 192 bits per
         // chip = 24 bytes per chip.
@@ -160,6 +230,10 @@
 
         // Configure the GSCLK output's frequency
         gsclk.period(1.0/GSCLK_SPEED);
+        
+        // mark that we need an initial update
+        newGSData = true;
+        needXlat = false;
      }
     
     // Start the clock running
@@ -236,55 +310,43 @@
     
     // Has new GS/DC data been loaded?
     volatile bool newGSData;
+    
+    // Do we need an XLAT signal on the next blanking interval?
+    volatile bool needXlat;
 
     // Function to reset the display and send the next chunks of data
     void reset()
     {
         // start the blanking cycle
         startBlank();
-
-        // If we have new GS data, send it now
-        if (true)
-        {
-            // Send the new grayscale data.
-            //
-            // Note that ideally, we'd do this during the new PWM cycle
-            // rather than during the blanking interval.  The TLC5940 is
-            // specifically designed to allow this.  However, in my testing,
-            // I found that sending new data during the PWM cycle was
-            // unreliable - it seemed to cause a fair amount of glitching,
-            // which as far as I can tell is signal noise coming from
-            // crosstalk between the grayscale clock signal and the 
-            // SPI signal.  This seems to be a common problem with
-            // daisy-chained TLC5940s.  It can in principle be solved with
-            // careful high-speed circuit design (good ground planes, 
-            // short leads, decoupling capacitors), and indeed I was able
-            // to improve stability to some extent with circuit tweaks,
-            // but I wasn't able to eliminate it entirely.  Moving the
-            // data refresh into the blanking interval, on the other 
-            // hand, seems to entirely eliminate any instability.
-            //
-            // update() will format the current grayscale data into our
-            // DMA transfer buffer and kick off the DMA transfer, then
-            // return.  At that point we can return from the interrupt,
-            // but WITHOUT ending the blanking cycle - we want to keep
-            // blanking the outputs until the DMA transfer finishes.  When
-            // the transfer is complete, the DMA controller will fire an
-            // interrupt that will trigger our dmaDone() callback, at 
-            // which point we'll finally complete the blanking cycle and
-            // start a new grayscale cycle.
+        
+#if DATA_UPDATE_INSIDE_BLANKING
+        // We're configured to send the new GS data entirely within
+        // the blanking interval.  Start the DMA transfer now, and
+        // return without ending the blanking interval.  The DMA
+        // completion interrupt handler will do that when the data
+        // update has completed.  
+        //
+        // Note that we do the data update/ unconditionally in the 
+        // send-during-blanking case, whether or not we have new GS 
+        // data.  This is because the update causes a 0.3% reduction 
+        // in brightness because of the elongated BLANK interval.
+        // That would be visible as a flicker on each update if we
+        // did updates on some cycles and not others.  By doing an
+        // update on every cycle, we make the brightness reduction
+        // uniform across time, which makes it less perceptible.
+        update();
+        
+#else // DATA_UPDATE_INSIDE_BLANKING
+        
+        // end the blanking interval
+        endBlank();
+        
+        // if we have pending grayscale data, start sending it
+        if (newGSData)
             update();
 
-            // the chips are now in sync with our data, so we have no more
-            // pending update
-            newGSData = false;
-        }
-        else
-        {
-            // no new grayscale data - just end the blanking cycle without
-            // a new XLAT
-            endBlank(false);
-        }
+#endif // DATA_UPDATE_INSIDE_BLANKING
     }
 
     void startBlank()
@@ -294,13 +356,16 @@
         blank = 1;        
     }
             
-    void endBlank(bool needxlat)
+    void endBlank()
     {
-        if (needxlat)
+        // if we've sent new grayscale data since the last blanking
+        // interval, latch it by asserting XLAT
+        if (needXlat)
         {
             // latch the new data while we're still blanked
             xlat = 1;
             xlat = 0;
+            needXlat = false;
         }
 
         // end the blanking interval and restart the grayscale clock
@@ -350,6 +415,9 @@
         
         // Start the DMA transfer
         sdma.start(nchips*24);
+        
+        // we've now cleared the new GS data
+        newGSData = false;
     }
 
     // Interrupt handler for DMA completion.  The DMA controller calls this
@@ -358,8 +426,15 @@
     // grayscale cycle.    
     void dmaDone()
     {
-        // when the DMA transfer is finished, start the next grayscale cycle
-        endBlank(true);
+        // mark that we need to assert XLAT to latch the new
+        // grayscale data during the next blanking interval
+        needXlat = true;
+        
+#if DATA_UPDATE_INSIDE_BLANKING
+        // we're doing the gs update within the blanking cycle, so end
+        // the blanking cycle now that the transfer has completed
+        endBlank();
+#endif
     }
 
 };
--- a/USBJoystick/USBJoystick.cpp	Sat Sep 26 02:15:59 2015 +0000
+++ b/USBJoystick/USBJoystick.cpp	Wed Oct 21 21:53:07 2015 +0000
@@ -94,18 +94,43 @@
     return send(&report);
 }
 
-bool USBJoystick::move(int16_t x, int16_t y) {
+bool USBJoystick::reportConfig(int numOutputs, int unitNo)
+{
+    HID_REPORT report;
+
+    // initially fill the report with zeros
+    memset(report.data, 0, sizeof(report.data));
+    
+    // Set the special status bits to indicate that it's a config report.
+    uint16_t s = 0x8800;
+    put(0, s);
+    
+    // write the number of configured outputs
+    put(2, numOutputs);
+    
+    // write the unit number
+    put(4, unitNo);
+    
+    // send the report
+    report.length = reportLen;
+    return send(&report);
+}
+
+bool USBJoystick::move(int16_t x, int16_t y) 
+{
      _x = x;
      _y = y;
      return update();
 }
 
-bool USBJoystick::setZ(int16_t z) {
+bool USBJoystick::setZ(int16_t z) 
+{
     _z = z;
     return update();
 }
  
-bool USBJoystick::buttons(uint32_t buttons) {
+bool USBJoystick::buttons(uint32_t buttons) 
+{
      _buttonsLo = (uint16_t)(buttons & 0xffff);
      _buttonsHi = (uint16_t)((buttons >> 16) & 0xffff);
      return update();
--- a/USBJoystick/USBJoystick.h	Sat Sep 26 02:15:59 2015 +0000
+++ b/USBJoystick/USBJoystick.h	Wed Oct 21 21:53:07 2015 +0000
@@ -124,6 +124,14 @@
          * @param pix pixel array
          */
          bool updateExposure(int &idx, int npix, const uint16_t *pix);
+         
+         /**
+         * Write a configuration report.
+         *
+         * @param numOutputs the number of configured output channels
+         * @param unitNo the device unit number
+         */
+         bool reportConfig(int numOutputs, int unitNo);
  
          /**
          * Write a state of the mouse
--- a/config.h	Sat Sep 26 02:15:59 2015 +0000
+++ b/config.h	Wed Oct 21 21:53:07 2015 +0000
@@ -8,6 +8,20 @@
 #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.
@@ -34,55 +48,83 @@
 #define ENABLE_JOYSTICK
 
 
-// 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
+// ---------------------------------------------------------------------------
+//
+// 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.
 
 
-// --------------------------------------------------------------------------
-// 
-// LedWiz default unit number.
+// 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
+
+
+// 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
+
+
+// ---------------------------------------------------------------------------
+//
+// 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; they set it to
-// unit 1 unless you specifically request a different number when you place
-// your order.
+// 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.  However, if we're set 
-// up as a secondary Pinscape controller with the joystick functions turned 
-// off, we'll use unit #9 instead.  
+// 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 you have in your system.  If you have a real
-// LedWiz, it's probably unit #1, since that's the default factory setting
-// that they'll give you if you didn't specifically ask for something else
-// when you ordered it.  If you have two real LedWiz's, they're probably 
-// units #1 and #2.  If you have three... well, I don't think anyone 
-// actually has three, but if you did it would probably be unit #3.  And so 
-// on.  That's why we start at #8: it seems really unlikely that anyone
-// with a pin cab has a real LedWiz unit #8.  On the off chance that you
-// do, simply change the setting here to a different unit number that's not 
-// already used in your system.
+// 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
@@ -106,6 +148,31 @@
    0x09;   // joystick disabled - assume we're a secondary, output-only KL25Z, so use #9
 #endif
 
+
+// --------------------------------------------------------------------------
+//
+// 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.
@@ -142,11 +209,11 @@
 //
 // 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.  USB gives us a
-// whole separate timing factor; we can't go much *faster* with USB than
-// sending a new report about every 10ms.  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.
+// 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
@@ -241,6 +308,35 @@
 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
+
+
 // --------------------------------------------------------------------------
 //
 // Pseudo "Launch Ball" button.
@@ -324,30 +420,45 @@
 // If you do add TLC5940 circuits to your controller hardware, use this
 // section to configure the connection to the KL25Z.
 //
-// Note that if you're using TLC5940 outputs, ALL of the outputs must go
-// through the TLC5940s - you can't mix TLC5940s and the default GPIO
-// device outputs.  This lets us take GPIO ports that we'd normally use
-// for device outputs and reassign them to control the TLC5940 hardware.
-
-// Uncomment this line if using TLC5940 chips
-//#define ENABLE_TLC5940
+// Note that when using the TLC5940, you can still also use some GPIO
+// pins for outputs as normal.  See ledWizPinMap[] for 
 
 // Number of TLC5940 chips you're using.  For a full LedWiz-compatible
-// setup, you need two of these chips, for 32 outputs.
-#define TLC5940_NCHIPS   4
+// setup, you need two of these chips, for 32 outputs.  The software
+// will handle up to 8.  The expansion board uses 4 of these chips; if
+// you're not using the expansion board, we assume you're not using
+// any of them.
+#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
 
 // 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.
+// selections, and that the GSCLK pin must be PWM-capable.  These defaults
+// all match the expansion board wiring.
 #define TLC5940_SIN    PTC6    // Must connect to SPI0 MOSI -> PTC6 or PTD2
 #define TLC5940_SCLK   PTC5    // Must connect to SPI0 SCLK -> PTC5 or PTD1; however, PTD1 isn't
                                //   recommended because it's hard-wired to the on-board blue LED
 #define TLC5940_XLAT   PTC10   // Any GPIO pin can be used
-#define TLC5940_BLANK  PTC0    // Any GPIO pin can be used
-#define TLC5940_GSCLK  PTD4    // Must be a PWM-capable pin
+#define TLC5940_BLANK  PTC7    // Any GPIO pin can be used
+#define TLC5940_GSCLK  PTA1    // Must be a PWM-capable pin
 
+// TLC5940 output power enable pin.  This is a GPIO pin that controls
+// a high-side transistor switch that controls power to the optos and
+// LEDs connected to the TLC5940 outputs.  This is a precaution against
+// powering the chip's output pins before Vcc is powered.  Vcc comes
+// from the KL25Z, so when our program is running, we know for certain
+// that Vcc is up.  This means that we can simply enable this pin any
+// time after entering our main().  Un-comment this line if using this
+// circuit.
+// #define TLC5940_PWRENA PTC11   // Any GPIO pin can be used
+#ifdef EXPANSION_BOARD
+# define TLC5940_PWRENA PTC11
+#endif
 
 #endif // CONFIG_H - end of include-once section (code below this point can be multiply included)
 
@@ -427,41 +538,55 @@
 
 // --------------------------------------------------------------------------
 //
-// LED-Wiz emulation output pin assignments - GPIO mode
+// LED-Wiz emulation output pin assignments
 //
-//   NOTE!  This section isn't used if you have TLC5940 outputs - ALL
-//   device outputs will be through the 5940s if you're using them.
-//   See the TLC5940 setup section above to configure your interface
-//   pins if you're using those chips.
+// 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 LED-Wiz protocol allows setting individual intensity levels
-// on all outputs, with 48 levels of intensity.  This can be used
-// to control lamp brightness and motor speeds, among other things.
-// Unfortunately, the KL25Z only has 10 PWM channels, so while we 
-// can support the full complement of 32 outputs, we can only provide 
-// PWM dimming/speed control on 10 of them.  The remaining outputs 
-// can only be switched fully on and fully off - we can't support
-// dimming on these, so they'll ignore any intensity level setting 
-// requested by the host.  Use these for devices that don't have any
-// use for intensity settings anyway, such as contactors and knockers.
+// 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.
 //
-// Ports with pins assigned as "NC" are not connected.  That is,
-// there's no physical pin for that LedWiz port number.  You can
-// send LedWiz commands to turn NC ports on and off, but doing so
-// will have no effect.  The reason we leave some ports unassigned
-// is that we don't have enough physical GPIO pins to fill out the
-// full LedWiz complement of 32 ports.  Many pins are already taken
-// for other purposes, such as button inputs or the plunger CCD
-// interface.
+// 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
+// assigned LED-Wiz port numbers is essentially arbitrary.  You can
 // customize this by changing the entries in the array below if you
 // wish to rearrange the pins for any reason.  Be aware that some
 // of the physical outputs are already used for other purposes
 // (e.g., some of the GPIO pins on header J10 are used for the
 // CCD sensor - but you can of course reassign those as well by
-// changing the corresponding declarations elsewhere in this module).
+// 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
@@ -508,47 +633,228 @@
 // array above to NC (for "not connected"), and plug the pin name into
 // a slot of your choice in the array below.
 //
-// Note: PTD1 (pin J2-12) should NOT be assigned as an LedWiz output,
-// as this pin is physically connected on the KL25Z to the on-board
-// indicator LED's blue segment.  This precludes any other use of
-// the pin.
-// 
+// 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 much precludes any other use 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 tlcPortNum to the non-zero port number,
+// starting at 1 for the first output on the first chip, 16 for the
+// last output on the first chip, 17 for the first output on the second
+// chip, and so on.  TLC ports are inherently PWM-capable only, so it's 
+// not necessary to set the PORT_IS_PWM flag for those.
+//
+
+// ledWizPortMap 'flags' bits - combine these with '|'
+const int PORT_IS_PWM     = 0x0001;  // this port is PWM-capable
+const int PORT_ACTIVE_LOW = 0x0002;  // use LOW voltage (0V) when port is ON
+
 struct {
-    PinName pin;
-    bool isPWM;
-} ledWizPortMap[32] = {
-    { PTA1, true },      // pin J1-2,  LW port 1  (PWM capable - TPM 2.0 = channel 9)
-    { PTA2, true },      // pin J1-4,  LW port 2  (PWM capable - TPM 2.1 = channel 10)
-    { PTD4, true },      // pin J1-6,  LW port 3  (PWM capable - TPM 0.4 = channel 5)
-    { PTA12, true },     // pin J1-8,  LW port 4  (PWM capable - TPM 1.0 = channel 7)
-    { PTA4, true },      // pin J1-10, LW port 5  (PWM capable - TPM 0.1 = channel 2)
-    { PTA5, true },      // pin J1-12, LW port 6  (PWM capable - TPM 0.2 = channel 3)
-    { PTA13, true },     // pin J2-2,  LW port 7  (PWM capable - TPM 1.1 = channel 13)
-    { PTD5, true },      // pin J2-4,  LW port 8  (PWM capable - TPM 0.5 = channel 6)
-    { PTD0, true },      // pin J2-6,  LW port 9  (PWM capable - TPM 0.0 = channel 1)
-    { PTD3, true },      // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4)
-    { PTD2, false },     // pin J2-8,  LW port 11
-    { PTC8, false },     // pin J1-14, LW port 12
-    { PTC9, false },     // pin J1-16, LW port 13
-    { PTC7, false },     // pin J1-1,  LW port 14
-    { PTC0, false },     // pin J1-3,  LW port 15
-    { PTC3, false },     // pin J1-5,  LW port 16
-    { PTC4, false },     // pin J1-7,  LW port 17
-    { PTC5, false },     // pin J1-9,  LW port 18
-    { PTC6, false },     // pin J1-11, LW port 19
-    { PTC10, false },    // pin J1-13, LW port 20
-    { PTC11, false },    // pin J1-15, LW port 21
-    { PTE0, false },     // pin J2-18, LW port 22
-    { NC, false },       // Not connected,  LW port 23
-    { NC, false },       // Not connected,  LW port 24
-    { NC, false },       // Not connected,  LW port 25
-    { NC, false },       // Not connected,  LW port 26
-    { NC, false },       // Not connected,  LW port 27
-    { NC, false },       // Not connected,  LW port 28
-    { NC, false },       // Not connected,  LW port 29
-    { NC, false },       // Not connected,  LW port 30
-    { NC, false },       // Not connected,  LW port 31
-    { NC, false }        // Not connected,  LW port 32
+    PinName pin;        // the GPIO pin assigned to this output; NC if not connected or a TLC5940 port
+    int flags;          // flags - a combination of PORT_xxx flag bits (see above)
+    int tlcPortNum;     // for TLC5940 ports, the TLC output number (1 to number of chips*16); otherwise 0
+} 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,  PORT_IS_PWM },      // pin J1-2,  LW port 1  (PWM capable - TPM 2.0 = channel 9)
+    { PTA2,  PORT_IS_PWM },      // pin J1-4,  LW port 2  (PWM capable - TPM 2.1 = channel 10)
+    { PTD4,  PORT_IS_PWM },      // pin J1-6,  LW port 3  (PWM capable - TPM 0.4 = channel 5)
+    { PTA12, PORT_IS_PWM },      // pin J1-8,  LW port 4  (PWM capable - TPM 1.0 = channel 7)
+    { PTA4,  PORT_IS_PWM },      // pin J1-10, LW port 5  (PWM capable - TPM 0.1 = channel 2)
+    { PTA5,  PORT_IS_PWM },      // pin J1-12, LW port 6  (PWM capable - TPM 0.2 = channel 3)
+    { PTA13, PORT_IS_PWM },      // pin J2-2,  LW port 7  (PWM capable - TPM 1.1 = channel 13)
+    { PTD5,  PORT_IS_PWM },      // pin J2-4,  LW port 8  (PWM capable - TPM 0.5 = channel 6)
+    { PTD0,  PORT_IS_PWM },      // pin J2-6,  LW port 9  (PWM capable - TPM 0.0 = channel 1)
+    { PTD3,  PORT_IS_PWM },      // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4)
+    { PTD2,  0 },                // pin J2-8,  LW port 11
+    { PTC8,  0 },                // pin J1-14, LW port 12
+    { PTC9,  0 },                // pin J1-16, LW port 13
+    { PTC7,  0 },                // pin J1-1,  LW port 14
+    { PTC0,  0 },                // pin J1-3,  LW port 15
+    { PTC3,  0 },                // pin J1-5,  LW port 16
+    { PTC4,  0 },                // pin J1-7,  LW port 17
+    { PTC5,  0 },                // pin J1-9,  LW port 18
+    { PTC6,  0 },                // pin J1-11, LW port 19
+    { PTC10, 0 },                // pin J1-13, LW port 20
+    { PTC11, 0 },                // pin J1-15, LW port 21
+    { PTE0,  0 },                // pin J2-18, LW port 22
+    { NC,    0 },                // Not connected,  LW port 23
+    { NC,    0 },                // Not connected,  LW port 24
+    { NC,    0 },                // Not connected,  LW port 25
+    { NC,    0 },                // Not connected,  LW port 26
+    { NC,    0 },                // Not connected,  LW port 27
+    { NC,    0 },                // Not connected,  LW port 28
+    { NC,    0 },                // Not connected,  LW port 29
+    { NC,    0 },                // Not connected,  LW port 30
+    { NC,    0 },                // Not connected,  LW port 31
+    { NC,    0 }                 // 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.  The
+    // "Button light" ports are good to about 1.5A, so these are most
+    // suitable for smaller loads like lamps, flashers, LEDs, etc.  The
+    // flipper and magnasave ports will only provide 20mA, so these are
+    // only usable for small LEDs.
+
+    // 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.
+    { NC,     0,    1 },         // TLC port 1,  LW output 1  - Flasher 1 R
+    { NC,     0,    2 },         // TLC port 2,  LW output 2  - Flasher 1 G
+    { NC,     0,    3 },         // TLC port 3,  LW output 3  - Flasher 1 B
+    { NC,     0,    4 },         // TLC port 4,  LW output 4  - Flasher 2 R
+    { NC,     0,    5 },         // TLC port 5,  LW output 5  - Flasher 2 G
+    { NC,     0,    6 },         // TLC port 6,  LW output 6  - Flasher 2 B
+    { NC,     0,    7 },         // TLC port 7,  LW output 7  - Flasher 3 R
+    { NC,     0,    8 },         // TLC port 8,  LW output 8  - Flasher 3 G
+    { NC,     0,    9 },         // TLC port 9,  LW output 9  - Flasher 3 B
+    { NC,     0,    10 },        // TLC port 10, LW output 10 - Flasher 4 R
+    { NC,     0,    11 },        // TLC port 11, LW output 11 - Flasher 4 G
+    { NC,     0,    12 },        // TLC port 12, LW output 12 - Flasher 4 B
+    { NC,     0,    13 },        // TLC port 13, LW output 13 - Flasher 5 R
+    { NC,     0,    14 },        // TLC port 14, LW output 14 - Flasher 5 G
+    { NC,     0,    15 },        // TLC port 15, LW output 15 - Flasher 5 B
+    { NC,     0,    16 },        // TLC port 16, LW output 16 - Strobe/Button light
+    { NC,     0,    17 },        // TLC port 17, LW output 17 - Button light 1
+    { NC,     0,    18 },        // TLC port 18, LW output 18 - Button light 2
+    { NC,     0,    19 },        // TLC port 19, LW output 19 - Button light 3
+    { NC,     0,    20 },        // TLC port 20, LW output 20 - Button light 4
+    { PTC8,   0,    0 },         // PTC8,        LW output 21 - Replay Knocker
+    { NC,     0,    21 },        // TLC port 21, LW output 22 - Contactor 1/General purpose
+    { NC,     0,    22 },        // TLC port 22, LW output 23 - Contactor 2/General purpose
+    { NC,     0,    23 },        // TLC port 23, LW output 24 - Contactor 3/General purpose
+    { NC,     0,    24 },        // TLC port 24, LW output 25 - Contactor 4/General purpose
+    { NC,     0,    25 },        // TLC port 25, LW output 26 - Contactor 5/General purpose
+    { NC,     0,    26 },        // TLC port 26, LW output 27 - Contactor 6/General purpose
+    { NC,     0,    27 },        // TLC port 27, LW output 28 - Contactor 7/General purpose
+    { NC,     0,    28 },        // TLC port 28, LW output 29 - Contactor 8/General purpose
+    { NC,     0,    29 },        // TLC port 29, LW output 30 - Contactor 9/General purpose
+    { NC,     0,    30 },        // TLC port 30, LW output 31 - Contactor 10/General purpose
+    { NC,     0,    31 },        // 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.
+    { NC,     0,    32 },        // TLC port 32, LW output 33 - Gear Motor/General purpose
+    { NC,     0,    33 },        // TLC port 33, LW output 34 - Fan/General purpose
+    { NC,     0,    34 },        // TLC port 34, LW output 35 - Beacon/General purpose
+    { NC,     0,    35 },        // TLC port 35, LW output 36 - Undercab RGB R/General purpose
+    { NC,     0,    36 },        // TLC port 36, LW output 37 - Undercab RGB G/General purpose
+    { NC,     0,    37 },        // TLC port 37, LW output 38 - Undercab RGB B/General purpose
+    { NC,     0,    38 },        // TLC port 38, LW output 39 - Bell/General purpose
+    { NC,     0,    39 },        // TLC port 39, LW output 40 - Chime 1/General purpose
+    { NC,     0,    40 },        // TLC port 40, LW output 41 - Chime 2/General purpose
+    { NC,     0,    41 },        // TLC port 41, LW output 42 - Chime 3/General purpose
+    { NC,     0,    42 },        // TLC port 42, LW output 43 - General purpose
+    { NC,     0,    43 },        // TLC port 43, LW output 44 - General purpose
+    { NC,     0,    44 },        // TLC port 44, LW output 45 - Button light 5
+    { NC,     0,    45 },        // TLC port 45, LW output 46 - Button light 6
+    { NC,     0,    46 },        // TLC port 46, LW output 47 - Button light 7
+    { NC,     0,    47 },        // TLC port 47, LW output 48 - Button light 8
+    { NC,     0,    49 },        // TLC port 49, LW output 49 - Flipper button RGB left R
+    { NC,     0,    50 },        // TLC port 50, LW output 50 - Flipper button RGB left G
+    { NC,     0,    51 },        // TLC port 51, LW output 51 - Flipper button RGB left B
+    { NC,     0,    52 },        // TLC port 52, LW output 52 - Flipper button RGB right R
+    { NC,     0,    53 },        // TLC port 53, LW output 53 - Flipper button RGB right G
+    { NC,     0,    54 },        // TLC port 54, LW output 54 - Flipper button RGB right B
+    { NC,     0,    55 },        // TLC port 55, LW output 55 - MagnaSave button RGB left R
+    { NC,     0,    56 },        // TLC port 56, LW output 56 - MagnaSave button RGB left G
+    { NC,     0,    57 },        // TLC port 57, LW output 57 - MagnaSave button RGB left B
+    { NC,     0,    58 },        // TLC port 58, LW output 58 - MagnaSave button RGB right R
+    { NC,     0,    59 },        // TLC port 59, LW output 59 - MagnaSave button RGB right G
+    { NC,     0,    60 }         // TLC port 60, LW output 60 - MagnaSave button RGB right B
+    
+#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,  0 },                // pin J1-14, LW port 1
+    { PTC9,  0 },                // pin J1-16, LW port 2
+    { PTC0,  0 },                // pin J1-3,  LW port 3
+    { PTC3,  0 },                // pin J1-5,  LW port 4
+    { PTC4,  0 },                // pin J1-7,  LW port 5
+    { PTA2,  PORT_IS_PWM },      // pin J1-4,  LW port 6   (PWM capable - TPM 2.1 = channel 10)
+    { PTD4,  PORT_IS_PWM },      // pin J1-6,  LW port 7   (PWM capable - TPM 0.4 = channel 5)
+    { PTA12, PORT_IS_PWM },      // pin J1-8,  LW port 8   (PWM capable - TPM 1.0 = channel 7)
+    { PTA4,  PORT_IS_PWM },      // pin J1-10, LW port 9   (PWM capable - TPM 0.1 = channel 2)
+    { PTA5,  PORT_IS_PWM }       // pin J1-12, LW port 10  (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
 };
 
 
--- a/main.cpp	Sat Sep 26 02:15:59 2015 +0000
+++ b/main.cpp	Wed Oct 21 21:53:07 2015 +0000
@@ -143,10 +143,24 @@
 //    The software can control a set of daisy-chained TLC5940 chips, which provide
 //    16 PWM outputs per chip.  Two of these chips give you the full complement
 //    of 32 output ports of an actual LedWiz, and four give you 64 ports, which
-//    should be plenty for nearly any virtual pinball project.
+//    should be plenty for nearly any virtual pinball project.  A private, extended
+//    version of the LedWiz protocol lets the host control the extra outputs, up to
+//    128 outputs per KL25Z (8 TLC5940s).  To take advantage of the extra outputs
+//    on the PC side, you need software that knows about the protocol extensions,
+//    which means you need the latest version of DirectOutput Framework (DOF).  VP
+//    uses DOF for its output, so VP will be able to use the added ports without any
+//    extra work on your part.  Older software (e.g., Future Pinball) that doesn't
+//    use DOF will still be able to use the LedWiz-compatible protocol, so it'll be
+//    able to control your first 32 ports (numbered 1-32 in the LedWiz scheme), but
+//    older software won't be able to address higher-numbered ports.  That shouldn't
+//    be a problem because older software wouldn't know what to do with the extra
+//    devices anyway - FP, for example, is limited to a pre-defined set of outputs.
+//    As long as you put the most common devices on the first 32 outputs, and use
+//    higher numbered ports for the less common devices that older software can't
+//    use anyway, you'll get maximum functionality out of software new and old.
 //
-//
-// The on-board LED on the KL25Z flashes to indicate the current device status:
+// STATUS LIGHTS:  The on-board LED on the KL25Z flashes to indicate the current 
+// device status.  The flash patterns are:
 //
 //    two short red flashes = the device is powered but hasn't successfully
 //        connected to the host via USB (either it's not physically connected
@@ -169,14 +183,14 @@
 //
 //    alternating blue/green = everything's working
 //
-// Software configuration: you can change option settings by sending special
+// 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 1 = 1  (0x01)
 //    byte 2 = new LedWiz unit number, 0x01 to 0x0f
 //    byte 3 = feature enable bit mask:
 //             0x01 = enable CCD (default = on)
@@ -188,14 +202,14 @@
 //
 //    length = 8 bytes
 //    byte 0 = 65 (0x41)
-//    byte 1 = 2 (0x02)
+//    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)
+//    byte 1 = 3  (0x03)
 //
 // We'll respond with a series of special reports giving the exposure status.
 // Each report has the following structure:
@@ -215,7 +229,33 @@
 // 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)
+
+
 #include "mbed.h"
 #include "math.h"
 #include "USBJoystick.h"
@@ -225,7 +265,6 @@
 #include "crc32.h"
 #include "TLC5940.h"
 
-// our local configuration file
 #define DECL_EXTERNS
 #include "config.h"
 
@@ -243,35 +282,27 @@
 inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); }
 
 
-// ---------------------------------------------------------------------------
-// USB device vendor ID, product ID, and version.  
+// --------------------------------------------------------------------------
+// 
+// USB product version number
 //
-// We use the vendor ID for the LedWiz, so that the PC-side software can
-// identify us as capable of performing LedWiz commands.  The LedWiz uses
-// a product ID value from 0xF0 to 0xFF; the last four bits identify the
-// unit number (e.g., product ID 0xF7 means unit #7).  This allows multiple
-// LedWiz units to be installed in a single PC; the software on the PC side
-// uses the unit number to route commands to the devices attached to each
-// unit.  On the real LedWiz, the unit number must be set in the firmware
-// at the factory; it's not configurable by the end user.  Most LedWiz's
-// ship with the unit number set to 0, but the vendor will set different
-// unit numbers if requested at the time of purchase.  So if you have a
-// single LedWiz already installed in your cabinet, and you didn't ask for
-// a non-default unit number, your existing LedWiz will be unit 0.
-//
-// Note that the USB_PRODUCT_ID value set here omits the unit number.  We
-// take the unit number from the saved configuration.  We provide a
-// configuration command that can be sent via the USB connection to change
-// the unit number, so that users can select the unit number without having
-// to install a different version of the software.  We'll combine the base
-// product ID here with the unit number to get the actual product ID that
-// we send to the USB controller.
-const uint16_t USB_VENDOR_ID = 0xFAFA;
-const uint16_t USB_PRODUCT_ID = 0x00F0;
-const uint16_t USB_VERSION_NO = 0x0006;
+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))
+
+
+// --------------------------------------------------------------------------
+//
 // Joystick axis report range - we report from -JOYMAX to +JOYMAX
+//
 #define JOYMAX 4096
 
 // --------------------------------------------------------------------------
@@ -357,16 +388,6 @@
 // for 32 outputs).  Every port in this mode has full PWM support.
 //
 
-// Figure the number of outputs.  If we're in the default LedWiz mode,
-// we have a fixed set of 32 outputs.  If we're in TLC5940 enhanced mode,
-// we have 16 outputs per chip.  To simplify the LedWiz compatibility code,
-// always use a minimum of 32 outputs even if we have fewer than two of the
-// TLC5940 chips.
-#if !defined(ENABLE_TLC5940) || (TLC_NCHIPS) < 2
-# define NUM_OUTPUTS   32
-#else
-# define NUM_OUTPUTS   ((TLC5940_NCHIPS)*16)
-#endif
 
 // Current starting output index for "PBA" messages from the PC (using
 // the LedWiz USB protocol).  Each PBA message implicitly uses the
@@ -385,10 +406,27 @@
     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
+{
+public:
+    LwUnusedOut() { }
+    virtual void set(float val) { }
+};
 
-#ifdef ENABLE_TLC5940
 
-// The TLC5940 interface object.
+#if TLC5940_NCHIPS
+//
+// 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);
 
@@ -410,7 +448,31 @@
     float prv;
 };
 
-#else // ENABLE_TLC5940
+// Inverted voltage version of TLC5940 class (Active Low - logical "on"
+// is represented by 0V on output)
+class Lw5940OutInv: public Lw5940Out
+{
+public:
+    Lw5940OutInv(int idx) : Lw5940Out(idx) { }
+    virtual void set(float val) { Lw5940Out::set(1.0 - val); }
+};
+
+#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) { }
+};
+
+class Lw5940OutInv: public Lw5940Out
+{
+public:
+    Lw5940OutInv(int idx) : Lw5940Out(idx) { }
+};
+
+#endif // TLC5940_NCHIPS
 
 // 
 // Default LedWiz mode - using on-board GPIO ports.  In this mode, we
@@ -434,6 +496,16 @@
     float prv;
 };
 
+// Inverted voltage PWM-capable GPIO port.  This is the Active Low
+// version of the port - logical "on" is represnted by 0V on the
+// GPIO pin.
+class LwPwmOutInv: public LwPwmOut
+{
+public:
+    LwPwmOutInv(PinName pin) : LwPwmOut(pin) { }
+    virtual void set(float val) { LwPwmOut::set(1.0 - val); }
+};
+
 // LwOut class for a Digital-Only (Non-PWM) GPIO port
 class LwDigOut: public LwOut
 {
@@ -448,21 +520,12 @@
     float prv;
 };
 
-#endif // ENABLE_TLC5940
-
-// 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
+// Inverted voltage digital out
+class LwDigOutInv: public LwDigOut
 {
 public:
-    LwUnusedOut() { }
-    virtual void set(float val) { }
+    LwDigOutInv(PinName pin) : LwDigOut(pin) { }
+    virtual void set(float val) { LwDigOut::set(1.0 - val); }
 };
 
 // Array of output physical pin assignments.  This array is indexed
@@ -472,48 +535,127 @@
 // 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.
-static LwOut *lwPin[NUM_OUTPUTS];
+static int numOutputs;
+static LwOut **lwPin;
+
+// 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, 
+// this is derived from the LedWiz state, and is updated on each pulse 
+// timer interrupt for lights in flashing states.  For outputs set by 
+// extended protocol messages, this is simply the brightness last set.
+static float *outLevel;
 
 // initialize the output pin array
 void initLwOut()
 {
-    for (int i = 0 ; i < countof(lwPin) ; ++i)
+    // 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, we use our own custom
+    // extended protocol that allows for many more ports.  In this case,
+    // we have 16 outputs per TLC5940, plus any assigned to GPIO pins.
+    
+    // start with 16 ports per TLC5940
+    numOutputs = TLC5940_NCHIPS * 16;
+    
+    // add outputs assigned to GPIO pins in the LedWiz-to-pin mapping
+    int i;
+    for (i = 0 ; i < countof(ledWizPortMap) ; ++i)
     {
-#ifdef ENABLE_TLC5940
-        // Set up a TLC5940 output.  If the output is within range of
-        // the connected number of chips (16 outputs per chip), assign it
-        // to the current index, otherwise leave it unattached.
-        if (i < (TLC5940_NCHIPS)*16)
-            lwPin[i] = new Lw5940Out(i);
-        else
-            lwPin[i] = new LwUnusedOut();
+        if (ledWizPortMap[i].pin != NC)
+            ++numOutputs;
+    }
+    
+    // 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;
+        
+    // allocate the pin array
+    lwPin = new LwOut*[numOutputs];    
+    
+    // allocate the current brightness array
+    outLevel = new float[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);
 
-#else // ENABLE_TLC5940
-        // Set up the GPIO pin.  If the pin is not connected ("NC" in the
-        // pin map), set up a dummy "unused" output for it.  If it's a
-        // real pin, set up a PWM-capable or Digital-Only output handler
-        // object, according to the pin type in the map.
-        PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC);
-        if (p == NC)
-            lwPin[i] = new LwUnusedOut();
-        else if (ledWizPortMap[i].isPWM)
-            lwPin[i] = new LwPwmOut(p);
-        else
-            lwPin[i] = new LwDigOut(p);
+    // assign all pins from the port map in config.h
+    for (i = 0 ; i < countof(ledWizPortMap) ; ++i)
+    {
+        // Figure out which type of pin to assign to this port:
+        //
+        // - If it has a valid GPIO pin (other than "NC"), create a PWM
+        //   or Digital output pin according to the port type.
+        //
+        // - If the pin has a TLC5940 port number, set up a TLC5940 port.
+        //
+        // - Otherwise, the pin is unconnected, so set up an unused out.
+        //
+        PinName p = ledWizPortMap[i].pin;
+        int flags = ledWizPortMap[i].flags;
+        int tlcPortNum = ledWizPortMap[i].tlcPortNum;
+        int isPwm = flags & PORT_IS_PWM;
+        int activeLow = flags & PORT_ACTIVE_LOW;
+        if (p != NC)
+        {
+            // This output is a GPIO - set it up as PWM or Digital, and 
+            // active high or low, as marked
+            if (isPwm)
+                lwPin[i] = activeLow ? new LwPwmOutInv(p) : new LwPwmOut(p);
+            else
+                lwPin[i] = activeLow ? new LwDigOutInv(p) : new LwDigOut(p);
+        }
+        else if (tlcPortNum != 0)
+        {
+            // It's a TLC5940 port.  Note that the port numbering in the map
+            // starts at 1, but internally we number the ports starting at 0,
+            // so subtract one to get the correct numbering.
+            lwPin[i] = activeLow ? new Lw5940OutInv(tlcPortNum-1) : new Lw5940Out(tlcPortNum-1);
             
-#endif // ENABLE_TLC5940
-
+            // mark this port as used, so that we don't reassign it when we
+            // fill out the remaining unassigned ports
+            tlcasi[tlcPortNum-1] = 1;
+        }
+        else
+        {
+            // it's not a GPIO or TLC5940 port -> it's not connected
+            lwPin[i] = new LwUnusedOut();
+        }
+        lwPin[i]->set(0);
     }
+    
+    // find the next unassigned tlc port
+    int tlcnxt;
+    for (tlcnxt = 0 ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ;
+    
+    // assign any remaining pins
+    for ( ; i < numOutputs ; ++i)
+    {
+        // If we have any more unassigned TLC5940 outputs, assign this LedWiz
+        // port to the next available TLC5940 output.  Otherwise make it
+        // unconnected.
+        if (tlcnxt < TLC5940_NCHIPS*16)
+        {
+            // we have a TLC5940 output available - assign it
+            lwPin[i] = new Lw5940Out(tlcnxt);
+            
+            // find the next unassigned TLC5940 output, for the next port
+            for (++tlcnxt ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ;
+        }
+        else
+        {
+            // no more ports available - set up this port as unconnected
+            lwPin[i] = new LwUnusedOut();
+        }
+    }
+    
+    // done with the temporary TLC5940 port assignment list
+    delete [] tlcasi;
 }
 
-// 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, 
-// this is derived from te LedWiz state, and is updated on each pulse 
-// timer interrupt for lights in flashing states.  For outputs set by 
-// extended protocol messages, this is simply the brightness last set.
-static float outLevel[NUM_OUTPUTS];
-
 // LedWiz output states.
 //
 // The LedWiz protocol has two separate control axes for each output.
@@ -1252,6 +1394,263 @@
     } d;
 };
 
+// ---------------------------------------------------------------------------
+//
+// Simple binary (on/off) input debouncer.  Requires an input to be stable 
+// for a given interval before allowing an update.
+//
+class Debouncer
+{
+public:
+    Debouncer(bool initVal, float tmin)
+    {
+        t.start();
+        this->stable = this->prv = initVal;
+        this->tmin = tmin;
+    }
+    
+    // Get the current stable value
+    bool val() const { return stable; }
+
+    // Apply a new sample.  This tells us the new raw reading from the
+    // input device.
+    void sampleIn(bool val)
+    {
+        // If the new raw reading is different from the previous
+        // raw reading, we've detected an edge - start the clock
+        // on the sample reader.
+        if (val != prv)
+        {
+            // we have an edge - reset the sample clock
+            t.reset();
+            
+            // this is now the previous raw sample for nxt time
+            prv = val;
+        }
+        else if (val != stable)
+        {
+            // The new raw sample is the same as the last raw sample,
+            // and different from the stable value.  This means that
+            // the sample value has been the same for the time currently
+            // indicated by our timer.  If enough time has elapsed to
+            // consider the value stable, apply the new value.
+            if (t.read() > tmin)
+                stable = val;
+        }
+    }
+    
+private:
+    // current stable value
+    bool stable;
+
+    // last raw sample value
+    bool prv;
+    
+    // elapsed time since last raw input change
+    Timer t;
+    
+    // Minimum time interval for stability, in seconds.  Input readings 
+    // must be stable for this long before the stable value is updated.
+    float tmin;
+};
+
+
+// ---------------------------------------------------------------------------
+//
+// Turn off all outputs and restore everything to the default LedWiz
+// state.  This sets outputs #1-32 to LedWiz profile value 48 (full
+// brightness) and switch state Off, sets all extended outputs (#33
+// and above) to zero brightness, and sets the LedWiz flash rate to 2.
+// This effectively restores the power-on conditions.
+//
+void allOutputsOff()
+{
+    // reset all LedWiz outputs to OFF/48
+    for (int i = 0 ; i < 32 ; ++i)
+    {
+        outLevel[i] = 0;
+        wizOn[i] = 0;
+        wizVal[i] = 48;
+        lwPin[i]->set(0);
+    }
+    
+    // reset all extended outputs (ports >32) to full off (brightness 0)
+    for (int i = 32 ; i < numOutputs ; ++i)
+    {
+        outLevel[i] = 0;
+        lwPin[i]->set(0);
+    }
+    
+    // restore default LedWiz flash rate
+    wizSpeed = 2;
+}
+
+// ---------------------------------------------------------------------------
+//
+// TV ON timer.  If this feature is enabled, we toggle a TV power switch
+// relay (connected to a GPIO pin) to turn on the cab's TV monitors shortly
+// after the system is powered.  This is useful for TVs that don't remember
+// their power state and don't turn back on automatically after being
+// unplugged and plugged in again.  This feature requires external
+// circuitry, which is built in to the expansion board and can also be
+// built separately - see the Build Guide for the circuit plan.
+//
+// Theory of operation: to use this feature, the cabinet must have a 
+// secondary PC-style power supply (PSU2) for the feedback devices, and
+// this secondary supply must be plugged in to the same power strip or 
+// switched outlet that controls power to the TVs.  This lets us use PSU2
+// as a proxy for the TV power state - when PSU2 is on, the TV outlet is 
+// powered, and when PSU2 is off, the TV outlet is off.  We use a little 
+// latch circuit powered by PSU2 to monitor the status.  The latch has a 
+// current state, ON or OFF, that we can read via a GPIO input pin, and 
+// we can set the state to ON by pulsing a separate GPIO output pin.  As 
+// long as PSU2 is powered off, the latch stays in the OFF state, even if 
+// we try to set it by pulsing the SET pin.  When PSU2 is turned on after 
+// being off, the latch starts receiving power but stays in the OFF state, 
+// since this is the initial condition when the power first comes on.  So 
+// if our latch state pin is reading OFF, we know that PSU2 is either off 
+// now or *was* off some time since we last checked.  We use a timer to 
+// check the state periodically.  Each time we see the state is OFF, we 
+// try pulsing the SET pin.  If the state still reads as OFF, we know 
+// that PSU2 is currently off; if the state changes to ON, though, we 
+// know that PSU2 has gone from OFF to ON some time between now and the 
+// previous check.  When we see this condition, we start a countdown
+// timer, and pulse the TV switch relay when the countdown ends.
+//
+// This scheme might seem a little convoluted, but it neatly handles
+// all of the different cases that can occur:
+//
+// - Most cabinets systems are set up with "soft" PC power switches, 
+//   so that the PC goes into "Soft Off" mode (ACPI state S5, in Windows
+//   parlance) when the user turns off the cabinet.  In this state, the
+//   motherboard supplies power to USB devices, so the KL25Z continues
+//   running without interruption.  The latch system lets us monitor
+//   the power state even when we're never rebooted, since the latch
+//   will turn off when PSU2 is off regardless of what the KL25Z is doing.
+//
+// - Some cabinet builders might prefer to use "hard" power switches,
+//   cutting all power to the cabinet, including the PC motherboard (and
+//   thus the KL25Z) every time the machine is turned off.  This also
+//   applies to the "soft" switch case above when the cabinet is unplugged,
+//   a power outage occurs, etc.  In these cases, the KL25Z will do a cold
+//   boot when the PC is turned on.  We don't know whether the KL25Z
+//   will power up before or after PSU2, so it's not good enough to 
+//   observe the *current* state of PSU2 when we first check - if PSU2
+//   were to come on first, checking the current state alone would fool
+//   us into thinking that no action is required, because we would never
+//   have known that PSU2 was ever off.  The latch handles this case by
+//   letting us see that PSU2 *was* off before we checked.
+//
+// - If the KL25Z is rebooted while the main system is running, or the 
+//   KL25Z is unplugged and plugged back in, we will correctly leave the 
+//   TVs as they are.  The latch state is independent of the KL25Z's 
+//   power or software state, so it's won't affect the latch state when
+//   the KL25Z is unplugged or rebooted; when we boot, we'll see that 
+//   the latch is already on and that we don't have to turn on the TVs.
+//   This is important because TV ON buttons are usually on/off toggles,
+//   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
+//   2 -> latch was off at last check, SET pulsed high
+//   3 -> SET pulsed low, ready to check status
+//   4 -> TV timer countdown in progress
+//   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;
+void TVTimerInt()
+{
+    // Check our internal state
+    switch (psu2_state)
+    {
+    case 1:
+        // Default state.  This means that the latch was on last
+        // time we checked or that this is the first check.  In
+        // 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)
+        {
+            // switch to OFF state
+            psu2_state = 2;
+            
+            // try setting the latch
+            psu2_status_set = 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_state = 3;
+        break;
+        
+    case 3:
+        // CHECK state: we pulsed SET, and we're now ready to see
+        // 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)
+        {
+            // The latch stuck, so PSU2 has transitioned from OFF
+            // to ON.  Start the TV countdown timer.
+            tv_timer.reset();
+            tv_timer.start();
+            psu2_state = 4;
+        }
+        else
+        {
+            // 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_state = 2;
+        }
+        break;
+        
+    case 4:
+        // TV timer countdown in progress.  If we've reached the
+        // delay time, pulse the relay.
+        if (tv_timer.read() >= TV_DELAY_TIME)
+        {
+            // turn on the relay for one timer interval
+            tv_relay = 1;
+            psu2_state = 5;
+        }
+        break;
+        
+    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;
+        psu2_state = 1;
+        break;
+    }
+}
+
+Ticker tv_ticker;
+void startTVTimer()
+{
+    // Set up our time routine to run every 1/4 second.  
+    tv_ticker.attach(&TVTimerInt, 0.25);
+}
+
+
+#else // ENABLE_TV_TIMER
+//
+// TV timer not used - just provide a dummy startup function
+void startTVTimer() { }
+//
+#endif // ENABLE_TV_TIMER
+
 
 // ---------------------------------------------------------------------------
 //
@@ -1269,12 +1668,31 @@
     ledG = 1;
     ledB = 1;
     
+    // start the TV timer, if applicable
+    startTVTimer();
+    
+    // we're not connected/awake yet
+    bool connected = false;
+    time_t connectChangeTime = time(0);
+    
+#if TLC5940_NCHIPS
+    // start the TLC5940 clock
+    for (int i = 0 ; i < numOutputs ; ++i) lwPin[i]->set(1.0);
+    tlc5940.start();
+    
+    // enable power to the TLC5940 opto/LED outputs
+# ifdef TLC5940_PWRENA
+    DigitalOut tlcPwrEna(TLC5940_PWRENA);
+    tlcPwrEna = 1;
+# endif
+#endif
+
     // initialize the LedWiz ports
     initLwOut();
     
     // initialize the button input ports
     initButtons();
-    
+
     // we don't need a reset yet
     bool needReset = false;
     
@@ -1314,7 +1732,7 @@
     // number from the saved configuration.
     MyUSBJoystick js(
         USB_VENDOR_ID, 
-        USB_PRODUCT_ID | cfg.d.ledWizUnitNo,
+        MAKE_USB_PRODUCT_ID(USB_VENDOR_ID, USB_PRODUCT_ID, cfg.d.ledWizUnitNo),
         USB_VERSION_NO);
         
     // last report timer - we use this to throttle reports, since VP
@@ -1360,11 +1778,6 @@
     bool reportPix = false;
 #endif
 
-#ifdef ENABLE_TLC5940
-    // start the TLC5940 clock
-    tlc5940.start();
-#endif
-
     // create our plunger sensor object
     PlungerSensor plungerSensor;
 
@@ -1467,7 +1880,11 @@
     // Device status.  We report this on each update so that the host config
     // tool can detect our current settings.  This is a bit mask consisting
     // of these bits:
-    //    0x01  -> plunger sensor enabled
+    //    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);
     
     // we're all set up - now just loop, processing sensor reports and 
@@ -1486,6 +1903,24 @@
             // 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) 
                 {
@@ -1497,11 +1932,27 @@
                     // 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
@@ -1571,21 +2022,88 @@
                         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);
+                    }
+                }
                 else 
                 {
-                    // LWZ-PBA - full state dump; each byte is one output
-                    // in the current bank.  pbaIdx keeps track of the bank;
-                    // this is incremented implicitly by each PBA message.
+                    // 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
+                    // 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                    
+                    // 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();
@@ -2112,10 +2630,28 @@
             printf("%d,%d\r\n", x, y);
 #endif
 
+        // check for connection status changes
+        int newConnected = js.isConnected() && !js.isSuspended();
+        if (newConnected != connected)
+        {
+            // give it a few seconds to stabilize
+            time_t tc = time(0);
+            if (tc - connectChangeTime > 3)
+            {
+                // note the new status
+                connected = newConnected;
+                connectChangeTime = tc;
+                
+                // if we're no longer connected, turn off all outputs
+                if (!connected)
+                    allOutputsOff();
+            }
+        }
+
         // provide a visual status indication on the on-board LED
         if (calBtnState < 2 && hbTimer.read_ms() > 1000) 
         {
-            if (js.isSuspended() || !js.isConnected())
+            if (!newConnected)
             {
                 // suspended - turn off the LED
                 ledR = 1;