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:
Tue Jan 05 05:23:07 2016 +0000
Parent:
37:ed52738445fc
Child:
39:b3815a1c3802
Commit message:
USB improvements

Changed in this revision

Pinscape_Controller.lib Show annotated file Show diff for this revision Revisions of this file
TLC5940/TLC5940.h 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
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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Pinscape_Controller.lib	Tue Jan 05 05:23:07 2016 +0000
@@ -0,0 +1,1 @@
+http://mbed.org/users/mjr/code/Pinscape_Controller/#ed52738445fc
--- a/TLC5940/TLC5940.h	Thu Dec 24 01:37:40 2015 +0000
+++ b/TLC5940/TLC5940.h	Tue Jan 05 05:23:07 2016 +0000
@@ -20,49 +20,47 @@
 #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.
+// Data Transmission Mode.
+//
+// NOTE!  This section contains a possible workaround to try if you're 
+// having data signal stability problems with your TLC5940 chips.  If
+// your chips are working properly, you can ignore this part!
 //
-// 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.
+// The software has two options for sending data updates to the chips:
+//
+// Mode 0:  Send data *during* the grayscale cycle.  This is the way the
+// chips are designed to be used.  While the grayscale clock is running,
+// we send data for the *next* cycle, then latch the updated data to the
+// output registers during the blanking interval at the end of the cycle.
 //
-// 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.
+// Mode 1:  Send data *between* grayscale cycles.  In this mode, we send
+// each complete update during a blanking period, then latch the update
+// and start the next grayscale cycle.  This isn't the way the chips were
+// intended to be used, but it works.  The disadvantage is that it requires
+// the blanking interval to be extended to be long enough for the full
+// data update (192 bits * the number of chips in the chain).  Since the
+// outputs are turned off for the entire blanking period, this reduces
+// the overall brightness/intensity of the outputs by reducing the duty
+// cycle.  The TLC5940 chips can't achieve 100% duty cycle to begin with,
+// since they require a certain minimum time in the blanking interval
+// between grayscale cycles; however, the minimum is so short that the
+// duty cycle is close to 100%.  With the full data transmission stuffed
+// into the blanking interval, we reduce the duty cycle further below
+// 100%.  With four chips in the chain, a 28 MHz data clock, and a
+// 500 kHz grayscale clock, the reduction is about 0.3%.
 //
-// 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
+// By default, we use Mode 0, because that's the timing model specified
+// by the manufacturer, and empirically it works well with the Pinscape 
+// Expansion boards.  
+// 
+// So what's the point of Mode 1?  In early testing, with a breadboard 
+// setup, I saw some problems with data signal stability, which manifested 
+// as sporadic flickering in the outputs.  Switching to Mode 1 improved
+// the signal stability considerably.  I'm therefore leaving this code
+// available as an option in case anyone runs into similar signal problems
+// and wants to try the alternative mode as a workaround.
+//
+#define DATA_UPDATE_INSIDE_BLANKING  0
 
 #include "mbed.h"
 #include "FastPWM.h"
@@ -99,31 +97,28 @@
   * isn't a factor.  E.g., at SPI=30MHz and GSCLK=500kHz, 
   * t(blank) is 8192us and t(refresh) is 25us.
   */
-#define SPI_SPEED 2800000
+#define SPI_SPEED 28000000
 
 /**
   * The rate at which the GSCLK pin is pulsed.   This also controls 
   * how often the reset function is called.   The reset function call
-  * rate is (1/GSCLK_SPEED) * 4096.  The maximum reliable rate is
+  * interval is (1/GSCLK_SPEED) * 4096.  The maximum reliable rate is
   * around 32Mhz.  It's best to keep this rate as low as possible:
   * the higher the rate, the higher the refresh() call frequency,
   * so the higher the CPU load.
   *
-  * The lower bound is probably dependent on the application.  For 
-  * driving LEDs, the limiting factor is that lower rates will increase
-  * visible flicker.  200 kHz seems to be a good lower bound for LEDs.  
-  * That provides about 48 cycles per second - that's about the same as
-  * the 50 Hz A/C cycle rate in many countries, which was itself chosen
-  * so that incandescent lights don't flicker.  (This rate is a function 
-  * of human eye physiology, which has its own refresh cycle of sorts
-  * that runs at about 50 Hz.  If you're designing an LED system for
-  * viewing by cats or drosophila, you might want to look into your
-  * target species' eye physiology, since the persistence of vision
-  * rate varies quite a bit from species to species.)  Flicker tends to 
-  * be more noticeable in LEDs than in incandescents, since LEDs don't
-  * have the thermal inertia of incandescents, so we use a slightly
-  * higher default here.  500 kHz = 122 full grayscale cycles per
-  * second = 122 reset calls per second (call every 8ms).
+  * The lower bound depends on the application.  For driving LEDs, 
+  * the limiting factor is that lower rates will increase visible flicker.
+  * A GSCLK speed of 200 kHz is about as low as you can go with LEDs 
+  * without excessive flicker.  That equals about 48 full grayscale
+  * cycles per second.  That might seem perfectly good in that it's 
+  * about the same as the standard 50Hz A/C cycle rate in many countries, 
+  * but the 50Hz rate was chosen to minimize visible flicker in 
+  * incandescent lamps, not LEDs.  LEDs need a higher rate because they 
+  * don't have thermal inertia as incandescents do.  The default we use 
+  * here is 500 kHz = 122 full grayscale cycles per second.  That seems
+  * to produce excellent visual results.  Higher rates would probably
+  * produce diminishing returns given that they also increase CPU load.
   */
 #define GSCLK_SPEED    500000
 
@@ -187,8 +182,7 @@
         // grayscale levels, but SPI is ultimately just a bit-level serial format,
         // so we can reformat the 12-bit blocks into 8-bit bytes to fit the 
         // KL25Z's limits.  This should work equally well on other microcontrollers 
-        // that are more flexible.  The TLC5940 appears to require polarity/phase
-        // format 0.
+        // that are more flexible.  The TLC5940 requires polarity/phase format 0.
         spi.format(8, 0);
         spi.frequency(SPI_SPEED);
         
@@ -211,6 +205,7 @@
         // Allocate a DMA buffer.  The transfer on each cycle is 192 bits per
         // chip = 24 bytes per chip.
         dmabuf = new char[nchips*24];
+        memset(dmabuf, 0, nchips*24);
         
         // Set up the Simple DMA interface object.  We use the DMA controller to
         // send grayscale data updates to the TLC5940 chips.  This lets the CPU
@@ -218,14 +213,14 @@
         // allows our blanking interrupt handler return almost immediately.
         // The DMA transfer is from our internal DMA buffer to SPI0, which is
         // the SPI controller physically connected to the TLC5940s.
-        sdma.source(dmabuf, 1);
-        sdma.destination(&(SPI0->D), 0, 8);
+        sdma.source(dmabuf, true, 8);
+        sdma.destination(&(SPI0->D), false, 8);
         sdma.trigger(Trigger_SPI0_TX);
         sdma.attach(this, &TLC5940::dmaDone);
         
         // Enable DMA on SPI0.  SimpleDMA doesn't do this for us; we have to
         // do it explicitly.  This is just a matter of setting bit 5 (TXDMAE)
-        // in the SPI controllers Control Register 2 (C2).
+        // in the SPI controller's Control Register 2 (C2).
         SPI0->C2 |= 0x20; // set bit 5 = 0x20 = TXDMAE in SPI0 control register 2
 
         // Configure the GSCLK output's frequency
@@ -257,7 +252,7 @@
         // in the timer clock vs the PWM clock that determines the GSCLCK
         // output to the TLC5940), which is far less noticeable than a 
         // constantly rotating phase misalignment.
-        reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
+        resetTimer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
     }
     
     ~TLC5940()
@@ -306,7 +301,7 @@
 
     // Timeout to end each PWM cycle.  This is a one-shot timer that we reset
     // on each cycle.
-    Timeout reset_timer;
+    Timeout resetTimer;
     
     // Has new GS/DC data been loaded?
     volatile bool newGSData;
@@ -336,15 +331,21 @@
         // update on every cycle, we make the brightness reduction
         // uniform across time, which makes it less perceptible.
         update();
+        sdma.start(nchips*24);
+
         
 #else // DATA_UPDATE_INSIDE_BLANKING
         
         // end the blanking interval
         endBlank();
         
-        // if we have pending grayscale data, start sending it
+        // if we have pending grayscale data, update the DMA data
         if (newGSData)
             update();
+                    
+        // send out the DMA contents
+        sdma.start(nchips*24);
+
 
 #endif // DATA_UPDATE_INSIDE_BLANKING
     }
@@ -373,7 +374,7 @@
         gsclk.write(.5);
         
         // set up the next blanking interrupt
-        reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
+        resetTimer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
     }
     
     void update()
@@ -413,9 +414,6 @@
             dmabuf[dst++] = (gs[i] & 0x0FF);
         }
         
-        // Start the DMA transfer
-        sdma.start(nchips*24);
-        
         // we've now cleared the new GS data
         newGSData = false;
     }
--- a/USBDevice.lib	Thu Dec 24 01:37:40 2015 +0000
+++ b/USBDevice.lib	Tue Jan 05 05:23:07 2016 +0000
@@ -1,1 +1,1 @@
-http://mbed.org/users/mjr/code/USBDevice/#884405d998bb
+http://mbed.org/users/mjr/code/USBDevice/#20bb47609697
--- a/USBJoystick/USBJoystick.cpp	Thu Dec 24 01:37:40 2015 +0000
+++ b/USBJoystick/USBJoystick.cpp	Tue Jan 05 05:23:07 2016 +0000
@@ -585,7 +585,34 @@
     return true;
 }
 
-// Handle messages on endpoint 4 - this is the keyboard interface.
+// Handle incoming messages on the joystick/LedWiz interface = endpoint 1.
+// This interface receives LedWiz protocol commands and commands using our
+// custom LedWiz protocol extensions.
+//
+// We simply queue the messages in our circular buffer for processing in 
+// the main loop.  The circular buffer object is designed for safe access
+// from the interrupt handler using the rule that only the interrupt 
+// handler can change the write pointer, and only the regular code can
+// change the read pointer.
+bool USBJoystick::EP1_OUT_callback()
+{
+    // Read this message
+    union {
+        LedWizMsg msg;
+        uint8_t buf[MAX_HID_REPORT_SIZE];
+    } buf;
+    uint32_t bytesRead = 0;
+    USBDevice::readEP(EP1OUT, buf.buf, &bytesRead, MAX_HID_REPORT_SIZE);
+    
+    // if it's the right length, queue it to our circular buffer
+    if (bytesRead == 8)
+        lwbuf.write(buf.msg);
+
+    // start the next read
+    return readStart(EP1OUT, 9);
+}
+
+// Handle incoming messages on the keyboard interface = endpoint 4.
 // 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 have to read them to keep the pipe open.
--- a/USBJoystick/USBJoystick.h	Thu Dec 24 01:37:40 2015 +0000
+++ b/USBJoystick/USBJoystick.h	Tue Jan 05 05:23:07 2016 +0000
@@ -8,6 +8,58 @@
  
 #include "USBHID.h"
 
+struct LedWizMsg
+{
+    uint8_t data[8];
+};
+
+// circular buffer for incoming reports
+template<class T, int cnt> class CircBuf
+{
+public:
+    CircBuf() 
+    {
+        iRead = iWrite = 0;
+    }
+
+    // Read an item from the buffer.  Returns true if an item was available,
+    // false if the buffer was empty.
+    bool read(T &result) 
+    {
+        if (iRead != iWrite)
+        {
+            memcpy(&result, &buf[iRead], sizeof(T));
+            iRead = advance(iRead);
+            return true;
+        }
+        else
+            return false;
+    }
+    
+    bool write(const T &item)
+    {
+        int nxt = advance(iWrite);
+        if (nxt != iRead)
+        {
+            memcpy(&buf[nxt], &item, sizeof(T));
+            iWrite = nxt;
+            return true;
+        }
+        else
+            return false;
+    }
+
+private:
+    int advance(int i)
+    {
+        return i + 1 >= cnt ? 0 : i + 1;
+    } 
+    
+    int iRead;
+    int iWrite;
+    T buf[cnt];
+};
+
 // keyboard interface report IDs 
 const uint8_t REPORT_ID_KB = 1;
 const uint8_t REPORT_ID_MEDIA = 2;
@@ -99,8 +151,15 @@
              _init();
              this->useKB = useKB;
              this->enableJoystick = enableJoystick;
+             reqTimer.start();
              connect(waitForConnect);
          };
+
+         /* read a report from the LedWiz buffer */
+         bool readLedWizMsg(LedWizMsg &msg)
+         {
+             return lwbuf.read(msg);
+         }
          
          /**
           * Send a keyboard report.  The argument gives the key state, in the standard
@@ -196,9 +255,12 @@
          virtual bool USBCallback_setConfiguration(uint8_t configuration);
          virtual bool USBCallback_setInterface(uint16_t interface, uint8_t alternate)
             { return interface == 0 || interface == 1; }
+            
+         virtual bool EP1_OUT_callback();
          virtual bool EP4_OUT_callback();
- 
+         
      private:
+         Timer reqTimer;
          bool enableJoystick;
          bool useKB;
          int16_t _x;                       
@@ -207,8 +269,11 @@
          uint16_t _buttonsLo;
          uint16_t _buttonsHi;
          uint16_t _status;
+
+         // Incoming LedWiz message buffer.  Each LedWiz message is exactly 8 bytes.
+         CircBuf<LedWizMsg, 64> lwbuf;
          
          void _init();                 
 };
  
-#endif
\ No newline at end of file
+#endif
--- a/USBProtocol.h	Thu Dec 24 01:37:40 2015 +0000
+++ b/USBProtocol.h	Tue Jan 05 05:23:07 2016 +0000
@@ -341,7 +341,14 @@
 //                    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)
+//                    5 = special button -> byte 6 is the special button code (see below)
 //          byte 6 = key code, which depends on the key type in byte 5
+//          byte 7 = flags - a combination of these bit values:
+//                    0x01 = pulse mode.  This reports a physical on/off switch's state
+//                           to the host as a brief key press whenever the switch changes
+//                           state.  This is useful for the VPinMAME Coin Door button,
+//                           which requires the End key to be pressed each time the
+//                           door changes state.
 //          
 // 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, 
@@ -378,7 +385,33 @@
 //                       (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)
+//                   0x01 = active-high output (0V on output turns attached device ON)
+//                   0x02 = noisemaker device: disable this output when "night mode" is engaged
+//
+//       Note that the on-board LED segments can be used as LedWiz output ports.  This
+//       is useful for testing a new installation with DOF or other PC software without
+//       having to connect any external devices.  Assigning the on-board LED segments to
+//       output ports overrides their normal status/diagnostic display use, so the normal
+//       status flash pattern won't appear when they're used this way.
+//
+//       Special port numbers:  if the LedWiz port number is one of these special values,
+//       the physical output is used for a special purpose.  These ports aren't visible
+//       to the PC as LedWiz ports; they're for internal use by the controller.  The
+//       special port numbers are:
+//
+//         254 = Night Mode indicator lamp.  This port is turned on when night mode
+//               is engaged, and turned off when night mode is disengaged.  This can
+//               be used, for example, to control an indicator LED inside a lighted
+//               momentary pushbutton switch used to activate night mode.  The light 
+//               provides visual feedback that the mode is turned on.
+//
+//
+// 14 -> Engage/disengage Night Mode.  When night mode is engaged, LedWiz outputs marked
+//       as "noisemaker" devices are disabled.  Byte 3 is 1 to engage night mode, 0 to
+//       cancel night mode.  Note that sending this command will override the current
+//       switch setting, if a toggle switch is configured to control Night Mode.  Toggling
+//       the switch will take control via the switch again.
+
 
 
 // --- PIN NUMBER MAPPINGS ---
@@ -407,44 +440,45 @@
 //    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
-
+//    17 = PTB18    (on-board LED Red segment - not exposed as a header pin)
+//    18 = PTB19    (on-board LED Green segment - not exposed as a header pin)
+//    19 = PTC0
+//    20 = PTC1
+//    21 = PTC2
+//    22 = PTC3
+//    23 = PTC4
+//    24 = PTC5
+//    25 = PTC6
+//    26 = PTC7
+//    27 = PTC8
+//    28 = PTC9
+//    29 = PTC10
+//    30 = PTC11
+//    31 = PTC12
+//    32 = PTC13
+//    33 = PTC16
+//    34 = PTC17
+//    35 = PTD0
+//    36 = PTD1     (on-board LED Blue segment)
+//    37 = PTD2
+//    38 = PTD3
+//    39 = PTD4
+//    40 = PTD5
+//    41 = PTD6
+//    42 = PTD7
+//    43 = PTE0
+//    44 = PTE1
+//    45 = PTE2
+//    46 = PTE3
+//    47 = PTE4
+//    48 = PTE5
+//    49 = PTE20
+//    50 = PTE21
+//    51 = PTE22
+//    52 = PTE23
+//    53 = PTE29
+//    54 = PTE30
+//    55 = PTE31
 
 // --- USB KEYBOARD SCAN CODES ---
 //
@@ -517,3 +551,19 @@
 //    0x02 = Volume Down
 //    0x04 = Mute on/off
 
+
+// --- SPECIAL BUTTON KEY CODES ---
+//
+// Use these for special keys in the button mappings
+//
+//    0x01 = Night mode switch, momentary switch mode.  Pushing this button 
+//           engages night mode, disabling all LedWiz outputs marked with the 
+//           "noisemaker" flag.  Other outputs are unaffected.  Pushing
+//           the button again disengages night mode.  Use this option if the
+//           physical button attached to the input is a momentary switch type.
+//
+//    0x02 = Night mode switch, toggle switch mode.  When this switch is on,
+//           night mode is engaged; when the switch is off, night mode is 
+//           disengaged.  Use this option if the physical switch attached to
+//           to the input is a toggle switch (not a momentary switch).
+
--- a/config.h	Thu Dec 24 01:37:40 2015 +0000
+++ b/config.h	Tue Jan 05 05:23:07 2016 +0000
@@ -48,6 +48,11 @@
 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)
+const int BtnTypeSpecial       = 5;      // special button (night mode switch, etc)
+
+// input button flags
+const uint8_t BtnFlagPulse     = 0x01;   // pulse mode - reports each change in the physical switch state
+                                         // as a brief press of the logical button/keyboard key
 
 // maximum number of input button mappings
 const int MAX_BUTTONS = 32;
@@ -62,11 +67,24 @@
 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
+const uint8_t PortFlagActiveLow  = 0x01; // physical output is active-low
+const uint8_t PortFlagNoisemaker = 0x02; // noisemaker device - disable when night mode is engaged
 
 // maximum number of output ports
 const int MAX_OUT_PORTS = 203;
 
+// port configuration data
+struct LedWizPortCfg
+{
+    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
+} __attribute__((packed));
+
+
 struct Config
 {
     // set all values to factory defaults
@@ -82,7 +100,7 @@
         // 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
+        usbProductID = 0x00F0;     // LedWiz product code for unit #1
         psUnitNo = 8;
         
         // enable joystick reports
@@ -107,39 +125,127 @@
         plunger.zbLaunchBall.btn = 0;
         
         // assume no TV ON switch
+#if 1
+        TVON.statusPin = PTD2;
+        TVON.latchPin = PTE0;
+        TVON.relayPin = PTD3;
+        TVON.delayTime = 7;
+#else
         TVON.statusPin = NC;
         TVON.latchPin = NC;
         TVON.relayPin = NC;
         TVON.delayTime = 0;
+#endif
         
         // assume no TLC5940 chips
+#if 1 // $$$
+        tlc5940.nchips = 2;
+#else
         tlc5940.nchips = 0;
+#endif
+
+        // default TLC5940 pin assignments
+        tlc5940.sin = PTC6;
+        tlc5940.sclk = PTC5;
+        tlc5940.xlat = PTC10;
+        tlc5940.blank = PTC7;
+        tlc5940.gsclk = PTA1;
         
         // assume no 74HC595 chips
         hc595.nchips = 0;
         
+        // default 74HC595 pin assignments
+        hc595.sin = PTA5;
+        hc595.sclk = PTA4;
+        hc595.latch = PTA12;
+        hc595.ena = PTD4;
+        
         // initially configure with no LedWiz output ports
         outPort[0].typ = PortTypeDisabled;
+        for (int i = 0 ; i < sizeof(specialPort)/sizeof(specialPort[0]) ; ++i)
+            specialPort[i].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
+
+#if 1            
+        for (int i = 0 ; i < 24 ; ++i) {
+            static int bp[] = {
+                21, // 1 = PTC2
+                12, // 2 = PTB3
+                11, // 3 = PTB2
+                10, // 4 = PTB1
+                54, // 5 = PTE30
+                30, // 6 = PTC11
+                48, // 7 = PTE5
+                47, // 8 = PTE4
+                46, // 9 = PTE3
+                45, // 10 = PTE2
+                16, // 11 = PTB11
+                15, // 12 = PTB10
+                14, // 13 = PTB9
+                13, // 14 = PTB8
+                31, // 15 = PTC12
+                32, // 16 = PTC13
+                33, // 17 = PTC16
+                34, // 18 = PTC17
+                7,  // 19 = PTA16
+                8,  // 20 = PTA17
+                55, // 21 = PTE31
+                41, // 22 = PTD6
+                42, // 23 = PTD7
+                44  // 24 = PTE1
+            };                
+            button[i].pin = bp[i];
+            button[i].typ = BtnTypeKey;
+            button[i].val = i+4;  // A, B, C...
+        }
+#endif
+        
+#if 0
+        button[23].typ = BtnTypeJoystick;
+        button[23].val = 5;  // B
+        button[23].flags = 0x01;  // pulse button
+        
+        button[22].typ = BtnTypeModKey;
+        button[22].val = 0x02;  // left shift
+        
+        button[21].typ = BtnTypeMedia;
+        button[21].val = 0x02;  // vol down
+        
+        button[20].typ = BtnTypeMedia;
+        button[20].val = 0x01;  // vol up
+#endif
+        
+#if 1 // $$$
+        {
+            int n = 0;
+            for (int i = 0 ; i < 32 ; ++i, ++n) {
+                outPort[n].typ = PortTypeTLC5940;
+                outPort[n].pin = i;
+                outPort[n].flags = 0;
+            }
+            outPort[n].typ = PortTypeGPIODig;
+            outPort[n].pin = 27; // PTC8
+            outPort[n++].flags = 0;
             
-        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
+            outPort[n].typ = PortTypeDisabled;
+        }
+#endif
+#if 0
+        outPort[0].typ = PortTypeGPIOPWM;
+        outPort[0].pin = 17;  // PTB18 = LED1 = Red LED
+        outPort[0].flags = PortFlagActiveLow;
+        outPort[1].typ = PortTypeGPIOPWM;
+        outPort[1].pin = 18;  // PTB19 = LED2 = Green LED
+        outPort[1].flags = PortFlagActiveLow;
+        outPort[2].typ = PortTypeGPIOPWM;
+        outPort[2].pin = 36;  // PTD1 = LED3 = Blue LED
+        outPort[2].flags = PortFlagActiveLow;
+
+        outPort[3].typ = PortTypeDisabled;
+#endif
     }        
     
     // --- USB DEVICE CONFIGURATION ---
@@ -327,20 +433,14 @@
         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
+        uint8_t flags;      // key flags - a bitwise combination of BtnFlagXxx values
         
-    } button[MAX_BUTTONS];
+    } __attribute__((packed))  button[MAX_BUTTONS] __attribute((packed));
     
 
     // --- 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];
+    LedWizPortCfg outPort[MAX_OUT_PORTS] __attribute__((packed));  // LedWiz & extended output ports 
+    LedWizPortCfg specialPort[1];          // special ports (Night Mode indicator, etc)
 };
 
 #endif
--- a/main.cpp	Thu Dec 24 01:37:40 2015 +0000
+++ b/main.cpp	Tue Jan 05 05:23:07 2016 +0000
@@ -20,40 +20,41 @@
 // The Pinscape Controller
 // A comprehensive input/output controller for virtual pinball machines
 //
-// 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.
+// This project implements an I/O controller for virtual pinball cabinets.  Its
+// function is to connect Windows pinball software, such as Visual Pinball, with
+// physical devices in the cabinet: buttons, sensors, and feedback devices that
+// create visual or mechanical effects during play.  
+//
+// The software can perform several different functions, which can be used 
+// individually or in any combination:
 //
-// The main functions are:
+//  - Nudge sensing.  This uses the KL25Z's on-board accelerometer to sense the
+//    motion of the cabinet when you nudge it.  Visual Pinball and other pinball 
+//    emulators on the PC have native handling for this type of input, so that 
+//    physical nudges on the cabinet turn into simulated effects on the virtual 
+//    ball.  The KL25Z measures accelerations as analog readings and is quite 
+//    sensitive, so the effect of a nudge on the simulation is proportional
+//    to the strength of the nudge.  Accelerations are reported to the PC via a 
+//    simulated joystick (using the X and Y axes); you just have to set some 
+//    preferences in your  pinball software to tell it that an accelerometer 
+//    is attached.
 //
-//  - 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 (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 a number of sensor options.  To use this feature,
+//  - Plunger position sensing, with mulitple 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.
+//    have native support for this type of input; as with the nudge setup, you just 
+//    have to set some options in VP to activate the plunger.
 //
 //    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.
+//    Note VP has built-in support for plunger devices like this one, but some VP
+//    tables can't use it without some additional scripting work.  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
@@ -75,14 +76,11 @@
 //    position to the fully retracted position only.)
 //
 //  - Button input wiring.  24 of the KL25Z's GPIO ports are mapped as digital inputs
-//    for buttons and switches.  The software reports these as joystick buttons when
-//    it sends reports to the PC.  These can be used to wire physical pinball-style
-//    buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous 
-//    switches (such as a tilt bob) to the PC.  Visual Pinball can use joystick buttons
-//    for input - you just have to assign a VP function to each button using VP's
-//    keyboard options dialog.  To wire a button physically, connect one terminal of
-//    the button switch to the KL25Z ground, and connect the other terminal to the
-//    the GPIO port you wish to assign to the button.
+//    for buttons and switches.  You can wire each input to a physical pinball-style
+//    button or switch, such as flipper buttons, Start buttons, coin chute switches,
+//    tilt bobs, and service buttons.  Each button can be configured to be reported
+//    to the PC as a joystick button or as a keyboard key (you can select which key
+//    is used for each button).
 //
 //  - LedWiz emulation.  The KL25Z can appear to the PC as an LedWiz device, and will
 //    accept and process LedWiz commands from the host.  The software can turn digital
@@ -134,6 +132,20 @@
 //    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.
 //
+//  - Night Mode control for output devices.  You can connect a switch or button
+//    to the controller to activate "Night Mode", which disables feedback devices
+//    that you designate as noisy.  You can designate outputs individually as being 
+//    included in this set or not.  This is useful if you want to play a game on 
+//    your cabinet late at night without waking the kids and annoying the neighbors.
+//
+//  - TV ON switch.  The controller can pulse a relay to turn on your TVs after
+//    power to the cabinet comes on, with a configurable delay timer.  This feature
+//    is for TVs that don't turn themselves on automatically when first plugged in.
+//    To use this feature, you have to build some external circuitry to allow the
+//    software to sense the power supply status, and you have to run wires to your
+//    TV's on/off button, which requires opening the case on your TV.  The Build
+//    Guide has details on the necessary circuitry and connections to the TV.
+//
 //
 //
 // STATUS LIGHTS:  The on-board LED on the KL25Z flashes to indicate the current 
@@ -146,16 +158,20 @@
 //
 //    short red flash = the host computer is in sleep/suspend mode
 //
+//    long red/yellow = USB connection problem.  The device still has a USB
+//        connection to the host, but data transmissions are failing.  This
+//        condition shouldn't ever occur; if it does, it probably indicates
+//        a bug in the device's USB software.  This display is provided to
+//        flag any occurrences for investigation.  You'll probably need to
+//        manually reset the device if this occurs.
+//
 //    long yellow/green = everything's working, but the plunger hasn't
-//        been calibrated; follow the calibration procedure described above.
-//        This flash mode won't appear if the CCD has been disabled.  Note
-//        that the device can't tell whether a CCD is physically attached;
-//        if you don't have a CCD attached, you can set the appropriate option 
-//        in config.h or use the  Windows config tool to disable the CCD 
-//        software features.
+//        been calibrated.  Follow the calibration procedure described in
+//        the project documentation.  This flash mode won't appear if there's
+//        no plunger sensor configured.
 //
-//    alternating blue/green = everything's working, and the plunger has
-//        been calibrated
+//    alternating blue/green = everything's working normally, and plunger
+//        calibration has been completed (or there's no plunger attached)
 //
 //
 // USB PROTOCOL:  please refer to USBProtocol.h for details on the USB
@@ -182,6 +198,13 @@
 
 
 // ---------------------------------------------------------------------------
+//
+// Forward declarations
+//
+void setNightMode(bool on);
+void toggleNightMode();
+
+// ---------------------------------------------------------------------------
 // utilities
 
 // number of elements in an array
@@ -209,20 +232,6 @@
 
 // ---------------------------------------------------------------------------
 //
-// On-board RGB LED elements - we use these for diagnostic displays.
-//
-// Note that LED3 (the blue segment) is hard-wired on the KL25Z to PTD1,
-// so PTD1 shouldn't be used for any other purpose (e.g., as a keyboard
-// input or a device output).  (This is kind of unfortunate in that it's 
-// one of only two ports exposed on the jumper pins that can be muxed to 
-// SPI0 SCLK.  This effectively limits us to PTC5 if we want to use the 
-// SPI capability.)
-//
-DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
-
-
-// ---------------------------------------------------------------------------
-//
 // Wire protocol value translations.  These translate byte values from
 // the USB protocol to local native format.
 //
@@ -251,12 +260,12 @@
 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
+        NC,    PTA1,  PTA2,  PTA4,  PTA5,  PTA12, PTA13, PTA16, PTA17, PTB0,    // 0-9
+        PTB1,  PTB2,  PTB3,  PTB8,  PTB9,  PTB10, PTB11, PTB18, PTB19, PTC0,    // 10-19
+        PTC1,  PTC2,  PTC3,  PTC4,  PTC5,  PTC6,  PTC7,  PTC8,  PTC9,  PTC10,   // 20-29
+        PTC11, PTC12, PTC13, PTC16, PTC17, PTD0,  PTD1,  PTD2,  PTD3,  PTD4,    // 30-39
+        PTD5,  PTD6,  PTD7,  PTE0,  PTE1,  PTE2,  PTE3,  PTE4,  PTE5,  PTE20,   // 40-49
+        PTE21, PTE22, PTE23, PTE29, PTE30, PTE31                                // 50-55
     };
     return (c < countof(p) ? p[c] : NC);
 }
@@ -264,6 +273,81 @@
 
 // ---------------------------------------------------------------------------
 //
+// On-board RGB LED elements - we use these for diagnostic displays.
+//
+// Note that LED3 (the blue segment) is hard-wired on the KL25Z to PTD1,
+// so PTD1 shouldn't be used for any other purpose (e.g., as a keyboard
+// input or a device output).  This is kind of unfortunate in that it's 
+// one of only two ports exposed on the jumper pins that can be muxed to 
+// SPI0 SCLK.  This effectively limits us to PTC5 if we want to use the 
+// SPI capability.
+//
+DigitalOut *ledR, *ledG, *ledB;
+
+// Show the indicated pattern on the diagnostic LEDs.  0 is off, 1 is
+// on, and -1 is no change (leaves the current setting intact).
+void diagLED(int r, int g, int b)
+{
+    if (ledR != 0 && r != -1) ledR->write(!r);
+    if (ledG != 0 && g != -1) ledG->write(!g);
+    if (ledB != 0 && b != -1) ledB->write(!b);
+}
+
+// check an output port assignment to see if it conflicts with
+// an on-board LED segment
+struct LedSeg 
+{ 
+    bool r, g, b; 
+    LedSeg() { r = g = b = false; } 
+
+    void check(LedWizPortCfg &pc)
+    {
+        // if it's a GPIO, check to see if it's assigned to one of
+        // our on-board LED segments
+        int t = pc.typ;
+        if (t == PortTypeGPIOPWM || t == PortTypeGPIODig)
+        {
+            // it's a GPIO port - check for a matching pin assignment
+            PinName pin = wirePinName(pc.pin);
+            if (pin == LED1)
+                r = true;
+            else if (pin == LED2)
+                g = true;
+            else if (pin == LED3)
+                b = true;
+        }
+    }
+};
+
+// Initialize the diagnostic LEDs.  By default, we use the on-board
+// RGB LED to display the microcontroller status.  However, we allow
+// the user to commandeer the on-board LED as an LedWiz output device,
+// which can be useful for testing a new installation.  So we'll check
+// for LedWiz outputs assigned to the on-board LED segments, and turn
+// off the diagnostic use for any so assigned.
+void initDiagLEDs(Config &cfg)
+{
+    // run through the configuration list and cross off any of the
+    // LED segments assigned to LedWiz ports
+    LedSeg l;
+    for (int i = 0 ; i < MAX_OUT_PORTS && cfg.outPort[i].typ != PortTypeDisabled ; ++i)
+        l.check(cfg.outPort[i]);
+    
+    // check the special ports
+    for (int i = 0 ; i < countof(cfg.specialPort) ; ++i)
+        l.check(cfg.specialPort[i]);
+    
+    // We now know which segments are taken for LedWiz use and which
+    // are free.  Create diagnostic ports for the ones not claimed for
+    // LedWiz use.
+    if (!l.r) ledR = new DigitalOut(LED1, 1);
+    if (!l.g) ledG = new DigitalOut(LED2, 1);
+    if (!l.b) ledB = new DigitalOut(LED3, 1);
+}
+
+
+// ---------------------------------------------------------------------------
+//
 // LedWiz emulation, and enhanced TLC5940 output controller
 //
 // There are two modes for this feature.  The default mode uses the on-board
@@ -354,7 +438,7 @@
     virtual void set(float val)
     {
         if (val != prv)
-           tlc5940->set(idx, (int)((prv = val) * 4095));
+           tlc5940->set(idx, (int)((prv = val) * 4095.0f));
     }
     int idx;
     float prv;
@@ -443,6 +527,13 @@
 static int numOutputs;
 static LwOut **lwPin;
 
+// Special output ports:
+//
+//    [0] = Night Mode indicator light
+//
+static LwOut *specialPin[1];
+
+
 // 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
@@ -451,13 +542,79 @@
 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, 
-// 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.
+// value from 0.0 for fully off to 1.0 for fully on.  This is used
+// for all extended ports (33 and above), and for any LedWiz port
+// with wizVal == 255.
 static float *outLevel;
 
+// Day/night mode override for an output.  For each output, this is
+// set to 1 if the output is enabled and 0 if the output is disabled
+// by a global mode control, such as Night Mode (currently Night Mode
+// is the only such global mode, but the idea could be extended to
+// other similar controls if other needs emerge).  To get the final
+// output level for each output, we simply multiply the outLevel value
+// for the port by this override vlaue.
+static uint8_t *modeLevel;
+
+// create a single output pin
+LwOut *createLwPin(LedWizPortCfg &pc, Config &cfg)
+{
+    // get this item's values
+    int typ = pc.typ;
+    int pin = pc.pin;
+    int flags = pc.flags;
+    int activeLow = flags & PortFlagActiveLow;
+
+    // create the pin interface object according to the port type        
+    LwOut *lwp;
+    switch (typ)
+    {
+    case PortTypeGPIOPWM:
+        // PWM GPIO port
+        lwp = new LwPwmOut(wirePinName(pin));
+        break;
+    
+    case PortTypeGPIODig:
+        // Digital GPIO port
+        lwp = new LwDigOut(wirePinName(pin));
+        break;
+    
+    case PortTypeTLC5940:
+        // TLC5940 port (if we don't have a TLC controller object, or it's not a valid
+        // output port number on the chips we have, create a virtual port)
+        if (tlc5940 != 0 && pin < cfg.tlc5940.nchips*16)
+            lwp = new Lw5940Out(pin);
+        else
+            lwp = new LwVirtualOut();
+        break;
+    
+    case PortType74HC595:
+        // 74HC595 port (if we don't have an HC595 controller object, or it's not a valid
+        // output number, create a virtual port)
+        if (hc595 != 0 && pin < cfg.hc595.nchips*8)
+            lwp = new Lw595Out(pin);
+        else
+            lwp = new LwVirtualOut();
+        break;
+
+    case PortTypeVirtual:
+    default:
+        // virtual or unknown
+        lwp = new LwVirtualOut();
+        break;
+    }
+    
+    // if it's Active Low, layer on an inverter
+    if (activeLow)
+        lwp = new LwInvertedOut(lwp);
+
+    // turn it off initially      
+    lwp->set(0);
+    
+    // return the pin
+    return lwp;
+}
+
 // initialize the output pin array
 void initLwOut(Config &cfg)
 {
@@ -481,55 +638,26 @@
     // allocate the pin array
     lwPin = new LwOut*[numOutputs];    
     
-    // Allocate the current brightness array.
-    outLevel = new float[numOutputs < 32 ? 32 : numOutputs];
+    // Allocate the current brightness array.  For these, allocate at
+    // least 32, so that we have enough for all LedWiz messages, but
+    // allocate the full set of actual ports if we have more than the
+    // LedWiz complement.
+    int minOuts = numOutputs < 32 ? 32 : numOutputs;
+    outLevel = new float[minOuts];
+    
+    // Allocate the mode override array
+    modeLevel = new uint8_t[minOuts];
+    
+    // start with all modeLevel values set to ON
+    memset(modeLevel, 1, minOuts);
     
     // 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;
-
-        // create the pin interface object according to the port type        
-        switch (typ)
-        {
-        case PortTypeGPIOPWM:
-            // PWM GPIO port
-            lwPin[i] = new LwPwmOut(wirePinName(pin));
-            break;
-        
-        case PortTypeGPIODig:
-            // Digital GPIO port
-            lwPin[i] = new LwDigOut(wirePinName(pin));
-            break;
+        lwPin[i] = createLwPin(cfg.outPort[i], cfg);
         
-        case PortTypeTLC5940:
-            // TLC5940 port
-            lwPin[i] = new Lw5940Out(pin);
-            break;
-        
-        case PortType74HC595:
-            // 74HC595 port
-            lwPin[i] = new Lw595Out(pin);
-            break;
-
-        case PortTypeVirtual:
-        default:
-            // virtual or unknown
-            lwPin[i] = new LwVirtualOut();
-            break;
-        }
-        
-        // if it's Active Low, layer an inverter
-        if (activeLow)
-            lwPin[i] = new LwInvertedOut(lwPin[i]);
-
-        // turn it off initially      
-        lwPin[i]->set(0);
-    }
+    // create the pin interface for each special port
+    for (i = 0 ; i < countof(cfg.specialPort) ; ++i)
+        specialPin[i] = createLwPin(cfg.specialPort[i], cfg);
 }
 
 // LedWiz output states.
@@ -612,7 +740,7 @@
         // makes us work properly with software that's expecting the
         // documented LedWiz behavior and therefore uses level 48 to
         // turn a contactor or relay fully on.
-        return val/48.0;
+        return val/48.0f;
     }
     else if (val == 49)
     {
@@ -623,29 +751,29 @@
         // the PC side (notably DOF) is aware of this and uses level 49
         // to mean "100% on".  To ensure compatibility with existing 
         // PC-side software, we need to recognize level 49.
-        return 1.0;
+        return 1.0f;
     }
     else if (val == 129)
     {
         //   129 = ramp up / ramp down
         return wizFlashCounter < 128 
-            ? wizFlashCounter/128.0 
-            : (256 - wizFlashCounter)/128.0;
+            ? wizFlashCounter/128.0f 
+            : (256 - wizFlashCounter)/128.0f;
     }
     else if (val == 130)
     {
         //   130 = flash on / off
-        return wizFlashCounter < 128 ? 1.0 : 0.0;
+        return wizFlashCounter < 128 ? 1.0f : 0.0f;
     }
     else if (val == 131)
     {
         //   131 = on / ramp down
-        return wizFlashCounter < 128 ? 1.0 : (255 - wizFlashCounter)/128.0;
+        return wizFlashCounter < 128 ? 1.0f : (255 - wizFlashCounter)/128.0f;
     }
     else if (val == 132)
     {
         //   132 = ramp up / on
-        return wizFlashCounter < 128 ? wizFlashCounter/128.0 : 1.0;
+        return wizFlashCounter < 128 ? wizFlashCounter/128.0f : 1.0f;
     }
     else
     {
@@ -654,7 +782,7 @@
         // LedWiz unit exhibits in response is accidental and could change
         // in a future version.  We'll treat all undefined values as equivalent 
         // to 48 (fully on).
-        return 1.0;
+        return 1.0f;
     }
 }
 
@@ -668,7 +796,7 @@
 // larger steps through the cycle on each interrupt.  Running
 // every 1/127 of a second = 8ms seems to be a pretty light load.
 Timeout wizPulseTimer;
-#define WIZ_PULSE_TIME_BASE  (1.0/127.0)
+#define WIZ_PULSE_TIME_BASE  (1.0f/127.0f)
 static void wizPulse()
 {
     // increase the counter by the speed increment, and wrap at 256
@@ -684,7 +812,7 @@
             uint8_t s = wizVal[i];
             if (s >= 129 && s <= 132)
             {
-                lwPin[i]->set(wizState(i));
+                lwPin[i]->set(wizState(i) * modeLevel[i]);
                 ena = true;
             }
         }
@@ -709,7 +837,7 @@
     for (int i = 0 ; i < numLwOutputs ; ++i)
     {
         pulse |= (wizVal[i] >= 129 && wizVal[i] <= 132);
-        lwPin[i]->set(wizState(i));
+        lwPin[i]->set(wizState(i) * modeLevel[i]);
     }
     
     // if any outputs are set to flashing mode, and the pulse timer
@@ -721,7 +849,24 @@
     if (hc595 != 0)
         hc595->update();
 }
+
+// Update all physical outputs.  This is called after a change to a global
+// setting that affects all outputs, such as engaging or canceling Night Mode.
+static void updateAllOuts()
+{
+    // uddate each LedWiz output
+    for (int i = 0 ; i < numLwOutputs ; ++i)
+        lwPin[i]->set(wizState(i) * modeLevel[i]);
         
+    // update each extended output
+    for (int i = 33 ; i < numOutputs ; ++i)
+        lwPin[i]->set(outLevel[i] * modeLevel[i]);
+        
+    // flush 74HC595 changes, if necessary
+    if (hc595 != 0)
+        hc595->update();
+}
+
 // ---------------------------------------------------------------------------
 //
 // Button input
@@ -730,19 +875,39 @@
 // button state
 struct ButtonState
 {
-    ButtonState() : di(NULL), pressed(0), t(0), js(0), keymod(0), keycode(0) { }
+    ButtonState()
+    {
+        di = NULL;
+        on = 0;
+        pressed = prev = 0;
+        dbstate = 0;
+        js = 0;
+        keymod = 0;
+        keycode = 0;
+        special = 0;
+        pulseState = 0;
+        pulseTime = 0.0f;
+    }
     
     // DigitalIn for the button
     DigitalIn *di;
-
-    // current on/off state
-    int pressed;
+    
+    // current PHYSICAL on/off state, after debouncing
+    uint8_t on;
     
-    // Sticky time remaining for current state.  When a
-    // state transition occurs, we set this to a debounce
-    // period.  Future state transitions will be ignored
-    // until the debounce time elapses.
-    float t;
+    // current LOGICAL on/off state as reported to the host.
+    uint8_t pressed;
+
+    // previous logical on/off state, when keys were last processed for USB 
+    // reports and local effects
+    uint8_t prev;
+    
+    // Debounce history.  On each scan, we shift in a 1 bit to the lsb if
+    // the physical key is reporting ON, and shift in a 0 bit if the physical
+    // key is reporting OFF.  We consider the key to have a new stable state
+    // if we have N consecutive 0's or 1's in the low N bits (where N is
+    // a parameter that determines how long we wait for transients to settle).
+    uint8_t dbstate;
     
     // joystick button mask for the button, if mapped as a joystick button
     uint32_t js;
@@ -754,11 +919,103 @@
     // media control key code
     uint8_t mediakey;
     
-
+    // special key code
+    uint8_t special;
+    
+    // Pulse mode: a button in pulse mode transmits a brief logical button press and
+    // release each time the attached physical switch changes state.  This is useful
+    // for cases where the host expects a key press for each change in the state of
+    // the physical switch.  The canonical example is the Coin Door switch in VPinMAME, 
+    // which requires pressing the END key to toggle the open/closed state.  This
+    // software design isn't easily implemented in a physical coin door, though -
+    // the easiest way to sense a physical coin door's state is with a simple on/off
+    // switch.  Pulse mode bridges that divide by converting a physical switch state
+    // to on/off toggle key reports to the host.
+    //
+    // Pulse state:
+    //   0 -> not a pulse switch - logical key state equals physical switch state
+    //   1 -> off
+    //   2 -> transitioning off-on
+    //   3 -> on
+    //   4 -> transitioning on-off
+    //
+    // Each state change sticks for a minimum period; when the timer expires,
+    // if the underlying physical switch is in a different state, we switch
+    // to the next state and restart the timer.  pulseTime is the amount of
+    // time remaining before we can make another state transition.  The state
+    // transitions require a complete cycle, 1 -> 2 -> 3 -> 4 -> 1...; this
+    // guarantees that the parity of the pulse count always matches the 
+    // current physical switch state when the latter is stable, which makes
+    // it impossible to "trick" the host by rapidly toggling the switch state.
+    // (On my original Pinscape cabinet, I had a hardware pulse generator
+    // for coin door, and that *was* possible to trick by rapid toggling.
+    // This software system can't be fooled that way.)
+    uint8_t pulseState;
+    float pulseTime;
+    
 } buttonState[MAX_BUTTONS];
 
-// timer for button reports
-static Timer buttonTimer;
+
+// Button data
+uint32_t jsButtons = 0;
+
+// Keyboard report state.  This tracks the USB keyboard state.  We can
+// report at most 6 simultaneous non-modifier keys here, plus the 8
+// modifier keys.
+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 };
+
+// button scan interrupt ticker
+Ticker buttonTicker;
+
+// Button scan interrupt handler.  We call this periodically via
+// a timer interrupt to scan the physical button states.  
+void scanButtons()
+{
+    // scan all button input pins
+    ButtonState *bs = buttonState;
+    for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
+    {
+        // if it's connected, check its physical state
+        if (bs->di != NULL)
+        {
+            // Shift the new state into the debounce history.  Note that
+            // the physical pin inputs are active low (0V/GND = ON), so invert 
+            // the reading by XOR'ing the low bit with 1.  And of course we
+            // only want the low bit (since the history is effectively a bit
+            // vector), so mask the whole thing with 0x01 as well.
+            uint8_t db = bs->dbstate;
+            db <<= 1;
+            db |= (bs->di->read() & 0x01) ^ 0x01;
+            bs->dbstate = db;
+            
+            // if we have all 0's or 1's in the history for the required
+            // debounce period, the key state is stable - check for a change
+            // to the last stable state
+            const uint8_t stable = 0x1F;   // 00011111b -> 5 stable readings
+            db &= stable;
+            if (db == 0 || db == stable)
+                bs->on = db;
+        }
+    }
+}
+
+// Button state transition timer.  This is used for pulse buttons, to
+// control the timing of the logical key presses generated by transitions
+// in the physical button state.
+Timer buttonTimer;
 
 // initialize the button inputs
 void initButtons(Config &cfg, bool &kbKeys)
@@ -776,6 +1033,10 @@
             // set up the GPIO input pin for this button
             bs->di = new DigitalIn(pin);
             
+            // if it's a pulse mode button, set the initial pulse state to Off
+            if (cfg.button[i].flags & BtnFlagPulse)
+                bs->pulseState = 1;
+            
             // note if it's a keyboard key of some kind (including media keys)
             uint8_t val = cfg.button[i].val;
             switch (cfg.button[i].typ)
@@ -806,37 +1067,19 @@
         }
     }
     
-    // start the button timer
-    buttonTimer.reset();
+    // start the button scan thread
+    buttonTicker.attach_us(scanButtons, 1000);
+
+    // start the button state transition timer
     buttonTimer.start();
 }
 
-// Button data
-uint32_t jsButtons = 0;
-
-// Keyboard state
-struct
+// Process the button state.  This sets up the joystick, keyboard, and
+// media control descriptors with the current state of keys mapped to
+// those HID interfaces, and executes the local effects for any keys 
+// mapped to special device functions (e.g., Night Mode).
+void processButtons()
 {
-    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; returns true if there are any button
-// state changes to report, false if not
-bool readButtons(Config &cfg)
-{
-    // no changes detected yet
-    bool changes = false;
-    
     // start with an empty list of USB key codes
     uint8_t modkeys = 0;
     uint8_t keys[7] = { 0, 0, 0, 0, 0, 0, 0 };
@@ -847,70 +1090,112 @@
     
     // start with no media keys pressed
     uint8_t mediakeys = 0;
-
-    // figure the time elapsed since the last scan
+    
+    // calculate the time since the last run
     float dt = buttonTimer.read();
-    
-    // reset the time for the next scan
     buttonTimer.reset();
-    
+
     // scan the button list
     ButtonState *bs = buttonState;
     for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
     {
-        // read this button
-        if (bs->di != 0)
+        // if it's a pulse-mode switch, get the virtual pressed state
+        if (bs->pulseState != 0)
         {
-            // deduct the elapsed time since the last update
-            // from the button's remaining sticky time
-            bs->t -= dt;
-            if (bs->t < 0)
-                bs->t = 0;
-            
-            // If the sticky time has elapsed, note the new physical
-            // state of the button.  If we still have sticky time
-            // remaining, ignore the physical state; the last state
-            // change persists until the sticky time elapses so that
-            // we smooth out any "bounce" (electrical transients that
-            // occur when the switch contact is opened or closed).
-            if (bs->t == 0)
+            // deduct the time to the next state change
+            bs->pulseTime -= dt;
+            if (bs->pulseTime < 0)
+                bs->pulseTime = 0;
+                
+            // if the timer has expired, check for state changes
+            if (bs->pulseTime == 0)
             {
-                // get the new physical state
-                int pressed = !bs->di->read();
-                
-                // update the button's logical state if this is a change
-                if (pressed != bs->pressed)
+                const float pulseLength = 0.2;
+                switch (bs->pulseState)
                 {
-                    // store the new state
-                    bs->pressed = pressed;
+                case 1:
+                    // off - if the physical switch is now on, start a button pulse
+                    if (bs->on) {
+                        bs->pulseTime = pulseLength;
+                        bs->pulseState = 2;
+                        bs->pressed = 1;
+                    }
+                    break;
                     
-                    // start a new sticky period for debouncing this
-                    // state change
-                    bs->t = 0.075;
+                case 2:
+                    // transitioning off to on - end the pulse, and start a gap
+                    // equal to the pulse time so that the host can observe the
+                    // change in state in the logical button
+                    bs->pulseState = 3;
+                    bs->pulseTime = pulseLength;
+                    bs->pressed = 0;
+                    break;
+                    
+                case 3:
+                    // on - if the physical switch is now off, start a button pulse
+                    if (!bs->on) {
+                        bs->pulseTime = pulseLength;
+                        bs->pulseState = 4;
+                        bs->pressed = 1;
+                    }
+                    break;
+                    
+                case 4:
+                    // transitioning on to off - end the pulse, and start a gap
+                    bs->pulseState = 1;
+                    bs->pulseTime = pulseLength;
+                    bs->pressed = 0;
+                    break;
                 }
             }
+        }
+        else
+        {
+            // not a pulse switch - the logical state is the same as the physical state
+            bs->pressed = bs->on;
+        }
 
-            // if it's pressed, add it to the appropriate key state list
-            if (bs->pressed)
+        // carry out any edge effects from buttons changing states
+        if (bs->pressed != bs->prev)
+        {
+            // check for special key transitions
+            switch (bs->special)
             {
-                // OR in the joystick button bit, mod key bits, and media key bits
-                newjs |= bs->js;
-                modkeys |= bs->keymod;
-                mediakeys |= bs->mediakey;
+            case 1:
+                // night mode momentary switch - when the button transitions from
+                // OFF to ON, invert night mode
+                if (bs->pressed)
+                    toggleNightMode();
+                break;
                 
-                // if it has a keyboard key, add the scan code to the active list
-                if (bs->keycode != 0 && nkeys < 7)
-                    keys[nkeys++] = bs->keycode;
+            case 2:
+                // night mode toggle switch - when the button changes state, change
+                // night mode to match the new state
+                setNightMode(bs->pressed);
+                break;
             }
+            
+            // remember the new state for comparison on the next run
+            bs->prev = bs->pressed;
+        }
+
+        // if it's pressed, add it to the appropriate key state list
+        if (bs->pressed)
+        {
+            // OR in the joystick button bit, mod key bits, and media key bits
+            newjs |= bs->js;
+            modkeys |= bs->keymod;
+            mediakeys |= bs->mediakey;
+            
+            // if it has a keyboard key, add the scan code to the active list
+            if (bs->keycode != 0 && nkeys < 7)
+                keys[nkeys++] = bs->keycode;
         }
     }
 
     // check for joystick button changes
     if (jsButtons != newjs)
-    {
-        changes = true;
         jsButtons = newjs;
-    }
     
     // Check for changes to the keyboard keys
     if (kbState.data[0] != modkeys
@@ -919,7 +1204,6 @@
     {
         // we have changes - set the change flag and store the new key data
         kbState.changed = true;
-        changes = true;
         kbState.data[0] = modkeys;
         if (nkeys <= 6) {
             // 6 or fewer simultaneous keys - report the key codes
@@ -938,11 +1222,7 @@
     {
         mediaState.changed = true;
         mediaState.data = mediakeys;
-        changes = true;
     }
-    
-    // return the change indicator
-    return changes;
 }
 
 // ---------------------------------------------------------------------------
@@ -1106,7 +1386,7 @@
          vx_ = vy_ = 0;
 
          // get the time since the last get() sample
-         float dt = tGet_.read_us()/1.0e6;
+         float dt = tGet_.read_us()/1.0e6f;
          tGet_.reset();
          
          // done manipulating the shared data
@@ -1277,7 +1557,7 @@
 //
 void clear_i2c()
 {
-    // assume a general-purpose output pin to the I2C clock
+    // set up general-purpose output pins to the I2C lines
     DigitalOut scl(MMA8451_SCL_PIN);
     DigitalIn sda(MMA8451_SDA_PIN);
     
@@ -1652,6 +1932,48 @@
 
 // ---------------------------------------------------------------------------
 //
+// NIGHT MODE flag.  When night mode is on, we disable all outputs
+// marked as "noisemakers" in the output configuration flags.
+int nightMode;
+
+// Update the global output mode settings
+static void globalOutputModeChange()
+{
+    // set the global modeLevel[] 
+    for (int i = 0 ; i < numOutputs ; ++i)
+    {
+        // assume the port will be on
+        uint8_t f = 1;
+        
+        // if night mode is in effect, and this is a noisemaker, disable it
+        if (nightMode && (cfg.outPort[i].flags & PortFlagNoisemaker) != 0)
+            f = 0;
+            
+        // set the final output port override value
+        modeLevel[i] = f;
+    }
+    
+    // update all outputs for the mode change
+    updateAllOuts();
+}
+
+// Turn night mode on or off
+static void setNightMode(bool on)
+{
+    nightMode = on;
+    globalOutputModeChange();
+    specialPin[0]->set(on ? 255.0 : 0.0);
+}
+
+// Toggle night mode
+static void toggleNightMode()
+{
+    setNightMode(!nightMode);
+}
+
+
+// ---------------------------------------------------------------------------
+//
 // Plunger Sensor
 //
 
@@ -1883,6 +2205,7 @@
                 cfg.button[idx].pin = data[3];
                 cfg.button[idx].typ = data[4];
                 cfg.button[idx].val = data[5];
+                cfg.button[idx].flags = data[6];
             }
         }
         break;
@@ -1904,8 +2227,21 @@
                 cfg.outPort[idx].pin = data[4];
                 cfg.outPort[idx].flags = data[5];
             }
+            else if (idx == 254)
+            {
+                // special ports
+                idx -= 254;
+                cfg.specialPort[idx].typ = data[3];
+                cfg.specialPort[idx].pin = data[4];
+                cfg.specialPort[idx].flags = data[5];
+            }
         }
         break;
+
+    case 14:
+        // engage/cancel Night Mode
+        setNightMode(data[2]);
+        break;
     }
 }
 
@@ -1914,256 +2250,261 @@
 // Handle an input report from the USB host.  Input reports use our extended
 // LedWiz protocol.
 //
-void handleInputMsg(HID_REPORT &report, USBJoystick &js, int &z)
+void handleInputMsg(uint8_t data[8], 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
+    //
+    if (data[0] == 64) 
     {
-        // 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)
         {
-            // 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);
+            // figure the on/off state bit for this output
+            if (bit == 0x100) {
+                bit = 1;
+                ++ri;
             }
             
-            // set the flash speed - enforce the value range 1-7
-            wizSpeed = data[5];
-            if (wizSpeed < 1)
-                wizSpeed = 1;
-            else if (wizSpeed > 7)
-                wizSpeed = 7;
+            // 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;
 
-            // update the physical outputs
+            // 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
+            diagLED(1, 0, 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 * modeLevel[i]);
+        }
+        
+        // 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();
-            
-            // 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
+        else
+            pbaIdx += 8;
+    }
+}
 
-                // 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;
-        }
-    }
+// ---------------------------------------------------------------------------
+//
+// Pre-connection diagnostic flasher
+//
+void preConnectFlasher()
+{
+    diagLED(1, 0, 0);
+    wait(0.05);
+    diagLED(0, 0, 0);
 }
 
 // ---------------------------------------------------------------------------
@@ -2177,17 +2518,21 @@
 //
 int main(void)
 {
-    // turn off our on-board indicator LED
-    ledR = 1;
-    ledG = 1;
-    ledB = 1;
+    printf("\r\nPinscape Controller starting\r\n"); // $$$ debug
     
     // clear the I2C bus for the accelerometer
     clear_i2c();
-    
+
     // load the saved configuration
     loadConfigFromFlash();
     
+    // initialize the diagnostic LEDs
+    initDiagLEDs(cfg);
+
+    // set up the pre-connected ticker
+    Ticker preConnectTicker;
+    preConnectTicker.attach(preConnectFlasher, 3);
+
     // start the TV timer, if applicable
     startTVTimer(cfg);
     
@@ -2204,7 +2549,11 @@
     // enable the 74HC595 chips, if present
     init_hc595(cfg);
     
-    // initialize the LedWiz ports
+    // Initialize the LedWiz ports.  Note that it's important to wait until
+    // after initializing the various off-board output port controller chip
+    // sybsystems (TLC5940, 74HC595), since pins attached to peripheral
+    // controllers will need to address their respective controller objects,
+    // which don't exit until we initialize those subsystems.
     initLwOut(cfg);
     
     // start the TLC5940 clock
@@ -2214,15 +2563,26 @@
     // 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(cfg.usbVendorID, cfg.usbProductID, USB_VERSION_NO, true, cfg.joystickEnabled, kbKeys);
+    
+    // we're now connected - kill the pre-connect ticker
+    preConnectTicker.detach();
         
-    // 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();
+    // Last report timer for the joytick interface.  We use the joystick timer 
+    // to throttle the report rate, because VP doesn't benefit from reports any 
+    // faster than about every 10ms.
+    Timer jsReportTimer;
+    jsReportTimer.start();
+    
+    // Time since we successfully sent a USB report.  This is a hacky workaround
+    // for sporadic problems in the USB stack that I haven't been able to figure
+    // out.  If we go too long without successfully sending a USB report, we'll
+    // try resetting the connection.
+    Timer jsOKTimer;
+    jsOKTimer.start();
     
     // set the initial status flags
     statusFlags = (cfg.plunger.enabled ? 0x01 : 0x00);
@@ -2355,18 +2715,12 @@
     // host requests
     for (;;)
     {
-        // Look for an incoming report.  Process a few input reports in
-        // a row, but stop after a few so that a barrage of inputs won't
-        // starve our output event processing.  Also, pause briefly between
-        // reads; allowing reads to occur back-to-back seems to occasionally 
-        // stall the USB pipeline (for reasons unknown; I'd fix the underlying 
-        // problem if I knew what it was).
-        HID_REPORT report;
-        for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr, wait_ms(1))
-        {
-            handleInputMsg(report, js, z);
-        }
+        // Process incoming reports
+        LedWizMsg lwmsg;
+        for (int rr = 0 ; rr < 64 && js.readLedWizMsg(lwmsg) ; ++rr) 
+            handleInputMsg(lwmsg.data, js, z);
        
+
         // check for plunger calibration
         if (calBtn != 0 && !calBtn->read())
         {
@@ -2460,19 +2814,15 @@
             if (calBtnLit) {
                 if (calBtnLed != 0)
                     calBtnLed->write(1);
-                ledR = 1;
-                ledG = 1;
-                ledB = 0;
+                diagLED(0, 0, 1);       // blue
             }
             else {
                 if (calBtnLed != 0)
                     calBtnLed->write(0);
-                ledR = 1;
-                ledG = 1;
-                ledB = 1;
+                diagLED(0, 0, 0);       // off
             }
         }
-        
+ 
         // If the plunger is enabled, and we're not already in a firing event,
         // and the last plunger reading had the plunger pulled back at least
         // a bit, watch for plunger release events until it's time for our next
@@ -2480,7 +2830,7 @@
         if (!firing && cfg.plunger.enabled && z >= JOYMAX/6)
         {
             // monitor the plunger until it's time for our next report
-            while (reportTimer.read_ms() < 15)
+            while (jsReportTimer.read_ms() < 15)
             {
                 // do a fast low-res scan; if it's at or past the zero point,
                 // start a firing event
@@ -2817,22 +3167,27 @@
             z0 = znew;
         }
 
-        // update the buttons
-        bool buttonsChanged = readButtons(cfg);
+        // process button updates
+        processButtons();
         
-        // send a keyboard report if we have new data to report
+        // send a keyboard report if we have new data
         if (kbState.changed)
         {
+            // send a keyboard report
             js.kbUpdate(kbState.data);
             kbState.changed = false;
         }
-
-        // send the media control report, if applicable
+        
+        // likewise for the media controller
         if (mediaState.changed)
         {
+            // send a media report
             js.mediaUpdate(mediaState.data);
             mediaState.changed = false;
         }
+        
+        // flag:  did we successfully send a joystick report on this round?
+        bool jsOK = false;
 
         // If it's been long enough since our last USB status report,
         // send the new report.  We throttle the report rate because
@@ -2840,7 +3195,7 @@
         // VP only wants to sync with the real world in 10ms intervals,
         // so reporting more frequently creates I/O overhead without 
         // doing anything to improve the simulation.
-        if (cfg.joystickEnabled && reportTimer.read_ms() > 10)
+        if (cfg.joystickEnabled && jsReportTimer.read_ms() > 10)
         {
             // read the accelerometer
             int xa, ya;
@@ -2867,10 +3222,10 @@
             accelRotate(x, y);
 
             // send the joystick report
-            js.update(x, y, zrep, jsButtons | simButtons, statusFlags);
+            jsOK = js.update(x, y, zrep, jsButtons | simButtons, statusFlags);
             
             // we've just started a new report interval, so reset the timer
-            reportTimer.reset();
+            jsReportTimer.reset();
         }
 
         // If we're in pixel dump mode, report all pixel exposure values
@@ -2885,9 +3240,17 @@
         
         // 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)
+        if (!cfg.joystickEnabled && jsReportTimer.read_ms() > 200)
         {
-            js.updateStatus(0);
+            jsOK = js.updateStatus(0);
+            jsReportTimer.reset();
+        }
+
+        // if we successfully sent a joystick report, reset the watchdog timer
+        if (jsOK) 
+        {
+            jsOKTimer.reset();
+            jsOKTimer.start();
         }
 
 #ifdef DEBUG_PRINTF
@@ -2912,45 +3275,60 @@
                     allOutputsOff();
             }
         }
-
+        
         // provide a visual status indication on the on-board LED
         if (calBtnState < 2 && hbTimer.read_ms() > 1000) 
         {
             if (!newConnected)
             {
                 // suspended - turn off the LED
-                ledR = 1;
-                ledG = 1;
-                ledB = 1;
+                diagLED(0, 0, 0);
 
                 // show a status flash every so often                
                 if (hbcnt % 3 == 0)
                 {
-                    // disconnected = red/red flash; suspended = red
+                    // disconnected = short red/red flash
+                    // suspended = short red flash
                     for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n)
                     {
-                        ledR = 0;
+                        diagLED(1, 0, 0);
                         wait(0.05);
-                        ledR = 1;
+                        diagLED(0, 0, 0);
                         wait(0.25);
                     }
                 }
             }
+            else if (jsOKTimer.read() > 5)
+            {
+                // too long without a USB report - show red/yellow
+                static bool dumped;
+                if (!dumped) {
+                    extern void USBDeviceStatusDump(void);
+                    USBDeviceStatusDump();
+                    dumped = true;
+                }
+                extern bool USB_DMAERR;
+                if (USB_DMAERR) {
+                    printf("USB DMAERR DETECTED!\r\n");
+                    //   js.disconnect();
+                    //   js.connect();
+                    //   USB_DMAERR = false;
+                }
+                jsOKTimer.stop();
+                hb = !hb;
+                diagLED(1, hb, 0);
+            }
             else if (cfg.plunger.enabled && !cfg.plunger.cal.calibrated)
             {
                 // connected, plunger calibration needed - flash yellow/green
                 hb = !hb;
-                ledR = (hb ? 0 : 1);
-                ledG = 0;
-                ledB = 1;
+                diagLED(hb, 1, 0);
             }
             else
             {
                 // connected - flash blue/green
                 hb = !hb;
-                ledR = 1;
-                ledG = (hb ? 0 : 1);
-                ledB = (hb ? 1 : 0);
+                diagLED(0, hb, !hb);
             }
             
             // reset the heartbeat timer