Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

Revision:
73:4e8ce0b18915
Parent:
72:884207c0aab0
Child:
74:822a92bc11d2
--- a/main.cpp	Wed Jan 04 20:14:12 2017 +0000
+++ b/main.cpp	Sat Jan 21 19:48:30 2017 +0000
@@ -163,6 +163,12 @@
 //        connection to the host (or so it appears to the device), but data 
 //        transmissions are failing.
 //
+//    medium blue flash = TV ON delay timer running.  This means that the
+//        power to the secondary PSU has just been turned on, and the TV ON
+//        timer is waiting for the configured delay time before pulsing the
+//        TV power button relay.  This is only shown if the TV ON feature is
+//        enabled.
+//
 //    long yellow/green = everything's working, but the plunger hasn't
 //        been calibrated.  Follow the calibration procedure described in
 //        the project documentation.  This flash mode won't appear if there's
@@ -297,56 +303,57 @@
 // they're so stingy, but it appears from empirical testing that we can 
 // create a static array up to about 9K before things get crashy.
 
+// Dynamic memory pool.  We'll reserve space for all dynamic 
+// allocations by creating a simple C array of bytes.  The size
+// of this array is the maximum number of bytes we can allocate
+// with malloc or operator 'new'.
+//
+// The maximum safe size for this array is, in essence, the
+// amount of physical KL25Z RAM left over after accounting for
+// static data throughout the rest of the program, the run-time
+// stack, and any other space reserved for compiler or MCU
+// overhead.  Unfortunately, it's not straightforward to
+// determine this analytically.  The big complication is that
+// the minimum stack size isn't easily predictable, as the stack
+// grows according to what the program does.  In addition, the
+// mbed platform tools don't give us detailed data on the
+// compiler/linker memory map.  All we get is a generic total
+// RAM requirement, which doesn't necessarily account for all
+// overhead (e.g., gaps inserted to get proper alignment for
+// particular memory blocks).  
+//
+// A very rough estimate: the total RAM size reported by the 
+// linker is about 3.5K (currently - that can obviously change 
+// as the project evolves) out of 16K total.  Assuming about a 
+// 3K stack, that leaves in the ballpark of 10K.  Empirically,
+// that seems pretty close.  In testing, we start to see some
+// instability at 10K, while 9K seems safe.  To be conservative,
+// we'll reduce this to 8K.
+//
+// Our measured total usage in the base configuration (22 GPIO
+// output ports, TSL1410R plunger sensor) is about 4000 bytes.
+// A pretty fully decked-out configuration (121 output ports,
+// with 8 TLC5940 chips and 3 74HC595 chips, plus the TSL1412R
+// sensor with the higher pixel count, and all expansion board
+// features enabled) comes to about 6700 bytes.  That leaves
+// us with about 1.5K free out of our 8K, so we still have a 
+// little more headroom for future expansion.
+//
+// For comparison, the standard mbed malloc() runs out of
+// memory at about 6K.  That's what led to this custom malloc:
+// we can just fit the base configuration into that 4K, but
+// it's not enough space for more complex setups.  There's
+// still a little room for squeezing out unnecessary space
+// from the mbed library code, but at this point I'd prefer
+// to treat that as a last resort, since it would mean having
+// to fork private copies of the libraries.
+static const size_t XMALLOC_POOL_SIZE = 8*1024;
+static char xmalloc_pool[XMALLOC_POOL_SIZE];
+static char *xmalloc_nxt = xmalloc_pool;
+static size_t xmalloc_rem = XMALLOC_POOL_SIZE;
+    
 void *xmalloc(size_t siz)
 {
-    // Dynamic memory pool.  We'll reserve space for all dynamic 
-    // allocations by creating a simple C array of bytes.  The size
-    // of this array is the maximum number of bytes we can allocate
-    // with malloc or operator 'new'.
-    //
-    // The maximum safe size for this array is, in essence, the
-    // amount of physical KL25Z RAM left over after accounting for
-    // static data throughout the rest of the program, the run-time
-    // stack, and any other space reserved for compiler or MCU
-    // overhead.  Unfortunately, it's not straightforward to
-    // determine this analytically.  The big complication is that
-    // the minimum stack size isn't easily predictable, as the stack
-    // grows according to what the program does.  In addition, the
-    // mbed platform tools don't give us detailed data on the
-    // compiler/linker memory map.  All we get is a generic total
-    // RAM requirement, which doesn't necessarily account for all
-    // overhead (e.g., gaps inserted to get proper alignment for
-    // particular memory blocks).  
-    //
-    // A very rough estimate: the total RAM size reported by the 
-    // linker is about 3.5K (currently - that can obviously change 
-    // as the project evolves) out of 16K total.  Assuming about a 
-    // 3K stack, that leaves in the ballpark of 10K.  Empirically,
-    // that seems pretty close.  In testing, we start to see some
-    // instability at 10K, while 9K seems safe.  To be conservative,
-    // we'll reduce this to 8K.
-    //
-    // Our measured total usage in the base configuration (22 GPIO
-    // output ports, TSL1410R plunger sensor) is about 4000 bytes.
-    // A pretty fully decked-out configuration (121 output ports,
-    // with 8 TLC5940 chips and 3 74HC595 chips, plus the TSL1412R
-    // sensor with the higher pixel count, and all expansion board
-    // features enabled) comes to about 6700 bytes.  That leaves
-    // us with about 1.5K free out of our 8K, so we still have a 
-    // little more headroom for future expansion.
-    //
-    // For comparison, the standard mbed malloc() runs out of
-    // memory at about 6K.  That's what led to this custom malloc:
-    // we can just fit the base configuration into that 4K, but
-    // it's not enough space for more complex setups.  There's
-    // still a little room for squeezing out unnecessary space
-    // from the mbed library code, but at this point I'd prefer
-    // to treat that as a last resort, since it would mean having
-    // to fork private copies of the libraries.
-    static char pool[8*1024];
-    static char *nxt = pool;
-    static size_t rem = sizeof(pool);
-    
     // align to a 4-byte increment
     siz = (siz + 3) & ~3;
     
@@ -360,7 +367,7 @@
     // context to handle failed allocations as fatal errors centrally.  We
     // can't recover from these automatically, so we have to resort to user
     // intervention, which we signal with the diagnostic LED flashes.
-    if (siz > rem)
+    if (siz > xmalloc_rem)
     {
         // halt with the diagnostic display (by looping forever)
         for (;;)
@@ -373,15 +380,17 @@
     }
 
     // get the next free location from the pool to return   
-    char *ret = nxt;
+    char *ret = xmalloc_nxt;
     
     // advance the pool pointer and decrement the remaining size counter
-    nxt += siz;
-    rem -= siz;
+    xmalloc_nxt += siz;
+    xmalloc_rem -= siz;
     
     // return the allocated block
     return ret;
-}
+};
+
+// our malloc() replacement
 
 // Overload operator new to call our custom malloc.  This ensures that
 // all 'new' allocations throughout the program (including library code)
@@ -542,15 +551,38 @@
 //
 DigitalOut *ledR, *ledG, *ledB;
 
+// Power on timer state for diagnostics.  We flash the blue LED when
+// nothing else is going on.  State 0-1 = off, 2-3 = on
+uint8_t powerTimerDiagState = 0;
+
 // Show the indicated pattern on the diagnostic LEDs.  0 is off, 1 is
 // on, and -1 is no change (leaves the current setting intact).
+static uint8_t diagLEDState = 0;
 void diagLED(int r, int g, int b)
 {
+    // remember the new state
+    diagLEDState = r | (g << 1) | (b << 2);
+    
+    // if turning everything off, use the power timer state instead, 
+    // applying it to the blue LED
+    if (diagLEDState == 0)
+        b = (powerTimerDiagState >= 2);
+        
+    // set the new state
     if (ledR != 0 && r != -1) ledR->write(!r);
     if (ledG != 0 && g != -1) ledG->write(!g);
     if (ledB != 0 && b != -1) ledB->write(!b);
 }
 
+// update the LEDs with the current state
+void diagLED(void)
+{
+    diagLED(
+        diagLEDState & 0x01,
+        (diagLEDState >> 1) & 0x01,
+        (diagLEDState >> 1) & 0x02);
+}
+
 // check an output port assignment to see if it conflicts with
 // an on-board LED segment
 struct LedSeg 
@@ -1053,13 +1085,56 @@
 static int numOutputs;
 static LwOut **lwPin;
 
-
-// Number of LedWiz emulation outputs.  This is the number of ports
-// accessible through the standard (non-extended) LedWiz protocol
-// messages.  The protocol has a fixed set of 32 outputs, but we
-// might have fewer actual outputs.  This is therefore set to the
-// lower of 32 or the actual number of outputs.
-static int numLwOutputs;
+// LedWiz output states.
+//
+// The LedWiz protocol has two separate control axes for each output.
+// One axis is its on/off state; the other is its "profile" state, which
+// is either a fixed brightness or a blinking pattern for the light.
+// The two axes are independent.
+//
+// Even though the original LedWiz protocol can only access 32 ports, we
+// maintain LedWiz state for every port, even if we have more than 32.  Our
+// extended protocol allows the client to select a bank of 32 outputs to
+// address via original protocol commands (SBA/PBA), which allows for one
+// Pinscape unit with more than 32 ports to be exposed on the client as
+// multiple virtual LedWiz units through a modified LEDWIZ.DLL interface
+// library.
+
+// Current LedWiz virtual unit: 0 = ports 1-32, 1 = ports 33-64, etc.
+// SBA and PBA messages address the block of ports set by this unit.
+uint8_t ledWizBank = 0;
+
+// on/off state for each LedWiz output
+static uint8_t *wizOn;
+
+// LedWiz "Profile State" (the LedWiz brightness level or blink mode)
+// for each LedWiz output.  If the output was last updated through an 
+// LedWiz protocol message, it will have one of these values:
+//
+//   0-48 = fixed brightness 0% to 100%
+//   49  = fixed brightness 100% (equivalent to 48)
+//   129 = ramp up / ramp down
+//   130 = flash on / off
+//   131 = on / ramp down
+//   132 = ramp up / on
+//
+// (Note that value 49 isn't documented in the LedWiz spec, but real
+// LedWiz units treat it as equivalent to 48, and some PC software uses
+// it, so we need to accept it for compatibility.)
+static uint8_t *wizVal;
+
+// LedWiz flash speed.  This is a value from 1 to 7 giving the pulse
+// rate for lights in blinking states.  Each bank of 32 lights has its
+// own pulse rate, so we need ceiling(number_of_physical_outputs/32)
+// entries here.  Note that we could allocate this dynamically, but
+// the maximum size is so small that it's more efficient to preallocate
+// it at the maximum size.
+static const int MAX_LW_BANKS = (MAX_OUT_PORTS+31)/32;
+static uint8_t wizSpeed[MAX_LW_BANKS];
+
+// Current LedWiz flash cycle counter.  This runs from 0 to 255
+// during each cycle. 
+static uint8_t wizFlashCounter[MAX_LW_BANKS];
 
 // Current absolute brightness levels for all outputs.  These are
 // DOF brightness level value, from 0 for fully off to 255 for fully
@@ -1219,19 +1294,23 @@
         }
     }
     
-    // the real LedWiz protocol can access at most 32 ports, or the
-    // actual number of outputs, whichever is lower
-    numLwOutputs = (numOutputs < 32 ? numOutputs : 32);
+    // allocate the pin array
+    lwPin = new LwOut*[numOutputs];
     
-    // allocate the pin array
-    lwPin = new LwOut*[numOutputs];    
+    // Allocate the current brightness array
+    outLevel = new uint8_t[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 uint8_t[minOuts];
+    // allocate the LedWiz output state arrays
+    wizOn = new uint8_t[numOutputs];
+    wizVal = new uint8_t[numOutputs];
+    
+    // initialize all LedWiz outputs to off and brightness 48
+    memset(wizOn, 0, numOutputs);
+    memset(wizVal, 48, numOutputs);
+    
+    // set all LedWiz virtual unit flash speeds to 2
+    for (i = 0 ; i < countof(wizSpeed) ; ++i)
+        wizSpeed[i] = 2;
     
     // create the pin interface object for each port
     for (i = 0 ; i < numOutputs ; ++i)
@@ -1260,7 +1339,7 @@
 // was used in the command.  On a legacy SBA or PBA, we switch to
 // LedWiz mode; on an extended output set message, we switch to
 // extended mode.  We remember the LedWiz and extended output state
-// for each LW ports (1-32) separately.  Any time the mode changes, 
+// for each LW port (1-32) separately.  Any time the mode changes, 
 // we set ports 1-32 back to the state for the new mode.
 //
 // The reasoning here is that any given client (on the PC) will use
@@ -1274,49 +1353,6 @@
 // program switching back and forth.
 static uint8_t ledWizMode = true;
 
-// LedWiz output states.
-//
-// The LedWiz protocol has two separate control axes for each output.
-// One axis is its on/off state; the other is its "profile" state, which
-// is either a fixed brightness or a blinking pattern for the light.
-// The two axes are independent.
-//
-// Note that the LedWiz protocol can only address 32 outputs, so the
-// wizOn and wizVal arrays have fixed sizes of 32 elements no matter
-// how many physical outputs we're using.
-
-// on/off state for each LedWiz output
-static uint8_t wizOn[32];
-
-// LedWiz "Profile State" (the LedWiz brightness level or blink mode)
-// for each LedWiz output.  If the output was last updated through an 
-// LedWiz protocol message, it will have one of these values:
-//
-//   0-48 = fixed brightness 0% to 100%
-//   49  = fixed brightness 100% (equivalent to 48)
-//   129 = ramp up / ramp down
-//   130 = flash on / off
-//   131 = on / ramp down
-//   132 = ramp up / on
-//
-// (Note that value 49 isn't documented in the LedWiz spec, but real
-// LedWiz units treat it as equivalent to 48, and some PC software uses
-// it, so we need to accept it for compatibility.)
-static uint8_t wizVal[32] = {
-    48, 48, 48, 48, 48, 48, 48, 48,
-    48, 48, 48, 48, 48, 48, 48, 48,
-    48, 48, 48, 48, 48, 48, 48, 48,
-    48, 48, 48, 48, 48, 48, 48, 48
-};
-
-// LedWiz flash speed.  This is a value from 1 to 7 giving the pulse
-// rate for lights in blinking states.
-static uint8_t wizSpeed = 2;
-
-// Current LedWiz flash cycle counter.  This runs from 0 to 255
-// during each cycle.
-static uint8_t wizFlashCounter = 0;
-
 // translate an LedWiz brightness level (0-49) to a DOF brightness
 // level (0-255)
 static const uint8_t lw_to_dof[] = {
@@ -1352,7 +1388,7 @@
         // the true 100% level.  (In the documentation, level 49 is
         // simply not a valid setting.)  Even so, we treat level 48 as
         // 100% on to match the documentation.  This won't be perfectly
-        // ocmpatible with the actual LedWiz, but it makes for such a
+        // compatible with the actual LedWiz, but it makes for such a
         // small difference in brightness (if the output device is an
         // LED, say) that no one should notice.  It seems better to
         // err in this direction, because while the difference in
@@ -1375,24 +1411,26 @@
     else if (val == 129)
     {
         // 129 = ramp up / ramp down
-        return wizFlashCounter < 128 
-            ? wizFlashCounter*2 + 1
-            : (255 - wizFlashCounter)*2;
+        const int c = wizFlashCounter[idx/32];
+        return c < 128 ? c*2 + 1 : (255 - c)*2;
     }
     else if (val == 130)
     {
         // 130 = flash on / off
-        return wizFlashCounter < 128 ? 255 : 0;
+        const int c = wizFlashCounter[idx/32];
+        return c < 128 ? 255 : 0;
     }
     else if (val == 131)
     {
         // 131 = on / ramp down
-        return wizFlashCounter < 128 ? 255 : (255 - wizFlashCounter)*2;
+        const int c = wizFlashCounter[idx/32];
+        return c < 128 ? 255 : (255 - c)*2;
     }
     else if (val == 132)
     {
         // 132 = ramp up / on
-        return wizFlashCounter < 128 ? wizFlashCounter*2 : 255;
+        const int c = wizFlashCounter[idx/32];
+        return c < 128 ? c*2 : 255;
     }
     else
     {
@@ -1418,13 +1456,16 @@
 #define WIZ_PULSE_TIME_BASE  (1.0f/127.0f)
 static void wizPulse()
 {
-    // increase the counter by the speed increment, and wrap at 256
-    wizFlashCounter += wizSpeed;
-    wizFlashCounter &= 0xff;
-    
-    // if we have any flashing lights, update them
-    int ena = false;
-    for (int i = 0 ; i < numLwOutputs ; ++i)
+    // update the flash counter in each bank
+    for (int bank = 0 ; bank < countof(wizFlashCounter) ; ++bank)
+    {
+        // increase the counter by the speed increment, and wrap at 256
+        wizFlashCounter[bank] = (wizFlashCounter[bank] + wizSpeed[bank]) & 0xff;
+    }
+
+    // look for outputs set to LedWiz flash modes 
+    int flashing = false;
+    for (int i = 0 ; i < numOutputs ; ++i)
     {
         if (wizOn[i])
         {
@@ -1432,7 +1473,7 @@
             if (s >= 129 && s <= 132)
             {
                 lwPin[i]->set(wizState(i));
-                ena = true;
+                flashing = true;
             }
         }
     }    
@@ -1443,7 +1484,7 @@
     // features when the host software doesn't care about the flashing 
     // modes.  For example, DOF never uses these modes, so there's no 
     // need for them when running Visual Pinball.
-    if (ena)
+    if (flashing)
         wizPulseTimer.attach(wizPulse, WIZ_PULSE_TIME_BASE);
 }
 
@@ -1453,7 +1494,7 @@
 {
     // update each output
     int pulse = false;
-    for (int i = 0 ; i < numLwOutputs ; ++i)
+    for (int i = 0 ; i < numOutputs ; ++i)
     {
         pulse |= (wizVal[i] >= 129 && wizVal[i] <= 132);
         lwPin[i]->set(wizState(i));
@@ -1473,15 +1514,41 @@
 // 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)
+    // uddate each output
+    for (int i = 0 ; i < numOutputs ; ++i)
         lwPin[i]->set(wizState(i));
         
-    // update each extended output
-    for (int i = numLwOutputs ; i < numOutputs ; ++i)
-        lwPin[i]->set(outLevel[i]);
+    // flush 74HC595 changes, if necessary
+    if (hc595 != 0)
+        hc595->update();
+}
+
+//
+// Turn off all outputs and restore everything to the default LedWiz
+// state.  This sets outputs #1-32 to LedWiz profile value 48 (full
+// brightness) and switch state Off, sets all extended outputs (#33
+// and above) to zero brightness, and sets the LedWiz flash rate to 2.
+// This effectively restores the power-on conditions.
+//
+void allOutputsOff()
+{
+    // reset all LedWiz outputs to OFF/48
+    for (int i = 0 ; i < numOutputs ; ++i)
+    {
+        outLevel[i] = 0;
+        wizOn[i] = 0;
+        wizVal[i] = 48;
+        lwPin[i]->set(0);
+    }
+    
+    // restore default LedWiz flash rate
+    for (int i = 0 ; i < countof(wizSpeed) ; ++i)
+        wizSpeed[i] = 2;
         
-    // flush 74HC595 changes, if necessary
+    // set bank 0
+    ledWizBank = 0;
+    
+    // flush changes to hc595, if applicable
     if (hc595 != 0)
         hc595->update();
 }
@@ -1496,7 +1563,6 @@
 {
     ButtonState()
     {
-        di = NULL;
         physState = logState = prevLogState = 0;
         virtState = 0;
         dbState = 0;
@@ -1520,7 +1586,7 @@
     }
     
     // DigitalIn for the button, if connected to a physical input
-    TinyDigitalIn *di;
+    TinyDigitalIn di;
     
     // Time of last pulse state transition.
     //
@@ -1640,31 +1706,21 @@
 void scanButtons()
 {
     // scan all button input pins
-    ButtonState *bs = buttonState;
-    for (int i = 0 ; i < nButtons ; ++i, ++bs)
+    ButtonState *bs = buttonState, *last = bs + nButtons;
+    for ( ; bs < last ; ++bs)
     {
-        // if this logical button is connected to a physical input, check 
-        // the GPIO pin state
-        if (bs->di != NULL)
-        {
-            // Shift the new state into the debounce history.  Note that
-            // 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->physState = db & 1;
-        }
+        // Shift the new state into the debounce history
+        uint8_t db = (bs->dbState << 1) | bs->di.read();
+        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, so apply the new
+        // physical state.  Note that the pins are active low, so the
+        // new button on/off state is the inverse of the GPIO state.
+        const uint8_t stable = 0x1F;   // 00011111b -> low 5 bits = last 5 readings
+        db &= stable;
+        if (db == 0 || db == stable)
+            bs->physState = !db;
     }
 }
 
@@ -1731,7 +1787,7 @@
             bs->cfgIndex = i;
 
             // set up the GPIO input pin for this button
-            bs->di = new TinyDigitalIn(pin);
+            bs->di.assignPin(pin);
             
             // if it's a pulse mode button, set the initial pulse state to Off
             if (cfg.button[i].flags & BtnFlagPulse)
@@ -2154,6 +2210,31 @@
     }
 }
 
+// Send a button status report
+void reportButtonStatus(USBJoystick &js)
+{
+    // start with all buttons off
+    uint8_t state[(MAX_BUTTONS+7)/8];
+    memset(state, 0, sizeof(state));
+
+    // pack the button states into bytes, one bit per button
+    ButtonState *bs = buttonState;
+    for (int i = 0 ; i < nButtons ; ++i, ++bs)
+    {
+        // get the physical state
+        int b = bs->physState;
+        
+        // pack it into the appropriate bit
+        int idx = bs->cfgIndex;
+        int si = idx / 8;
+        int shift = idx & 0x07;
+        state[si] |= b << shift;
+    }
+    
+    // send the report
+    js.reportButtonStatus(MAX_BUTTONS, state);
+}
+
 // ---------------------------------------------------------------------------
 //
 // Customization joystick subbclass
@@ -2506,13 +2587,14 @@
          vx_ = vy_ = 0;
 
          // get the time since the last get() sample
-         float dt = tGet_.read_us()/1.0e6f;
+         int dtus = tGet_.read_us();
          tGet_.reset();
          
          // done manipulating the shared data
          __enable_irq();
          
          // adjust the readings for the integration time
+         float dt = dtus/1000000.0f;
          vx /= dt;
          vy /= dt;
          
@@ -2773,40 +2855,6 @@
 
 // ---------------------------------------------------------------------------
 //
-// Turn off all outputs and restore everything to the default LedWiz
-// state.  This sets outputs #1-32 to LedWiz profile value 48 (full
-// brightness) and switch state Off, sets all extended outputs (#33
-// and above) to zero brightness, and sets the LedWiz flash rate to 2.
-// This effectively restores the power-on conditions.
-//
-void allOutputsOff()
-{
-    // reset all LedWiz outputs to OFF/48
-    for (int i = 0 ; i < numLwOutputs ; ++i)
-    {
-        outLevel[i] = 0;
-        wizOn[i] = 0;
-        wizVal[i] = 48;
-        lwPin[i]->set(0);
-    }
-    
-    // reset all extended outputs (ports >32) to full off (brightness 0)
-    for (int i = numLwOutputs ; i < numOutputs ; ++i)
-    {
-        outLevel[i] = 0;
-        lwPin[i]->set(0);
-    }
-    
-    // restore default LedWiz flash rate
-    wizSpeed = 2;
-    
-    // flush changes to hc595, if applicable
-    if (hc595 != 0)
-        hc595->update();
-}
-
-// ---------------------------------------------------------------------------
-//
 // TV ON timer.  If this feature is enabled, we toggle a TV power switch
 // relay (connected to a GPIO pin) to turn on the cab's TV monitors shortly
 // after the system is powered.  This is useful for TVs that don't remember
@@ -2882,14 +2930,38 @@
 //   3 -> SET pulsed low, ready to check status
 //   4 -> TV timer countdown in progress
 //   5 -> TV relay on
-int psu2_state = 1;
+uint8_t psu2_state = 1;
+
+// TV relay state.  The TV relay can be controlled by the power-on
+// timer and directly from the PC (via USB commands), so keep a
+// separate state for each:
+//
+//   0x01 -> turned on by power-on timer
+//   0x02 -> turned on by USB command
+uint8_t tv_relay_state = 0x00;
+const uint8_t TV_RELAY_POWERON = 0x01;
+const uint8_t TV_RELAY_USB     = 0x02;
+
+// TV ON switch relay control
+DigitalOut *tv_relay;
 
 // PSU2 power sensing circuit connections
 DigitalIn *psu2_status_sense;
 DigitalOut *psu2_status_set;
 
-// TV ON switch relay control
-DigitalOut *tv_relay;
+// Apply the current TV relay state
+void tvRelayUpdate(uint8_t bit, bool state)
+{
+    // update the state
+    if (state)
+        tv_relay_state |= bit;
+    else
+        tv_relay_state &= ~bit;
+    
+    // set the relay GPIO to the new state
+    if (tv_relay != 0)
+        tv_relay->write(tv_relay_state != 0);
+}
 
 // Timer interrupt
 Ticker tv_ticker;
@@ -2937,6 +3009,10 @@
             tv_timer.reset();
             tv_timer.start();
             psu2_state = 4;
+            
+            // start the power timer diagnostic flashes
+            powerTimerDiagState = 2;
+            diagLED();
         }
         else
         {
@@ -2954,16 +3030,24 @@
         if (tv_timer.read() >= tv_delay_time)
         {
             // turn on the relay for one timer interval
-            tv_relay->write(1);
+            tvRelayUpdate(TV_RELAY_POWERON, true);
             psu2_state = 5;
         }
+        
+        // flash the power time diagnostic every two interrupts
+        powerTimerDiagState = (powerTimerDiagState + 1) & 0x03;
+        diagLED();
         break;
         
     case 5:
         // TV timer relay on.  We pulse this for one interval, so
         // it's now time to turn it off and return to the default state.
-        tv_relay->write(0);
+        tvRelayUpdate(TV_RELAY_POWERON, false);
         psu2_state = 1;
+        
+        // done with the diagnostic flashes
+        powerTimerDiagState = 0;
+        diagLED();
         break;
     }
 }
@@ -2984,13 +3068,56 @@
         psu2_status_sense = new DigitalIn(wirePinName(cfg.TVON.statusPin));
         psu2_status_set = new DigitalOut(wirePinName(cfg.TVON.latchPin));
         tv_relay = new DigitalOut(wirePinName(cfg.TVON.relayPin));
-        tv_delay_time = cfg.TVON.delayTime/100.0;
+        tv_delay_time = cfg.TVON.delayTime/100.0f;
     
         // Set up our time routine to run every 1/4 second.  
         tv_ticker.attach(&TVTimerInt, 0.25);
     }
 }
 
+// TV relay manual control timer.  This lets us pulse the TV relay
+// under manual control, separately from the TV ON timer.
+Ticker tv_manualTicker;
+void TVManualInt()
+{
+    tv_manualTicker.detach();
+    tvRelayUpdate(TV_RELAY_USB, false);
+}
+
+// Operate the TV ON relay.  This allows manual control of the relay
+// from the PC.  See protocol message 65 submessage 11.
+//
+// Mode:
+//    0 = turn relay off
+//    1 = turn relay on
+//    2 = pulse relay 
+void TVRelay(int mode)
+{
+    // if there's no TV relay control pin, ignore this
+    if (tv_relay == 0)
+        return;
+    
+    switch (mode)
+    {
+    case 0:
+        // relay off
+        tvRelayUpdate(TV_RELAY_USB, false);
+        break;
+        
+    case 1:
+        // relay on
+        tvRelayUpdate(TV_RELAY_USB, true);
+        break;
+        
+    case 2:
+        // Pulse the relay.  Turn it on, then set our timer for 250ms.
+        tvRelayUpdate(TV_RELAY_USB, true);
+        tv_manualTicker.attach(&TVManualInt, 0.25);
+        break;
+    }
+}
+
+
 // ---------------------------------------------------------------------------
 //
 // In-memory configuration data structure.  This is the live version in RAM
@@ -3652,11 +3779,122 @@
 
 private:
 
-#if 1
+// Plunger data filtering mode:  optionally apply filtering to the raw 
+// plunger sensor readings to try to reduce noise in the signal.  This
+// is designed for the TSL1410/12 optical sensors, where essentially all
+// of the noise in the signal comes from lack of sharpness in the shadow
+// edge.  When the shadow is blurry, the edge detector has to pick a pixel,
+// even though the edge is actually a gradient spanning several pixels.
+// The edge detection algorithm decides on the exact pixel, but whatever
+// the algorithm, the choice is going to be somewhat arbitrary given that
+// there's really no one pixel that's "the edge" when the edge actually
+// covers multiple pixels.  This can make the choice of pixel sensitive to
+// small changes in exposure and pixel respose from frame to frame, which
+// means that the reported edge position can move by a pixel or two from
+// one frame to the next even when the physical plunger is perfectly still.
+// That's the noise we're talking about.
+//
+// We previously applied a mild hysteresis filter to the signal to try to
+// eliminate this noise.  The filter tracked the average over the last
+// several samples, and rejected readings that wandered within a few
+// pixels of the average.  If a certain number of readings moved away from
+// the average in the same direction, even by small amounts, the filter
+// accepted the changes, on the assumption that they represented actual
+// slow movement of the plunger.  This filter was applied after the firing
+// detection.
+//
+// I also tried a simpler filter that rejected changes that were too fast
+// to be physically possible, as well as changes that were very close to
+// the last reported position (i.e., simple hysteresis).  The "too fast"
+// filter was there to reject spurious readings where the edge detector
+// mistook a bad pixel value as an edge.  
+//
+// The new "mode 2" edge detector (see ccdSensor.h) seems to do a better
+// job of rejecting pixel-level noise by itself than the older "mode 0"
+// algorithm did, so I removed the filtering entirely.  Any filtering has
+// some downsides, so it's better to reduce noise in the underlying signal
+// as much as possible first.  It seems possible to get a very stable signal
+// now with a combination of the mode 2 edge detector and optimizing the
+// physical sensor arrangement, especially optimizing the light source to
+// cast as sharp as shadow as possible and adjusting the brightness to
+// maximize bright/dark contrast in the image.
+//
+//   0 = No filtering (current default)
+//   1 = Filter the data after firing detection using moving average
+//       hysteresis filter (old version, used in most 2016 releases)
+//   2 = Filter the data before firing detection using simple hysteresis
+//       plus spurious "too fast" motion rejection
+//
+#define PLUNGER_FILTERING_MODE  0
+
+#if PLUNGER_FILTERING_MODE == 0
     // Disable all filtering
     void applyPreFilter(PlungerReading &r) { }
     int applyPostFilter() { return z; }
-#elif 1
+#elif PLUNGER_FILTERING_MODE == 1
+    // Apply pre-processing filter.  This filter is applied to the raw
+    // value coming off the sensor, before calibration or fire-event
+    // processing.
+    void applyPreFilter(PlungerReading &r)
+    {
+    }
+    
+    // Figure the next post-processing filtered value.  This applies a
+    // hysteresis filter to the last raw z value and returns the 
+    // filtered result.
+    int applyPostFilter()
+    { 
+        if (firing <= 1)
+        {
+            // Filter limit - 5 samples.  Once we've been moving
+            // in the same direction for this many samples, we'll
+            // clear the history and start over.
+            const int filterMask = 0x1f;
+            
+            // figure the last average
+            int lastAvg = int(filterSum / filterN);
+            
+            // figure the direction of this sample relative to the average,
+            // and shift it in to our bit mask of recent direction data
+            if (z != lastAvg)
+            {
+                // shift the new direction bit into the vector
+                filterDir <<= 1;
+                if (z > lastAvg) filterDir |= 1;
+            }
+            
+            // keep only the last N readings, up to the filter limit
+            filterDir &= filterMask;
+            
+            // if we've been moving consistently in one direction (all 1's
+            // or all 0's in the direction history vector), reset the average
+            if (filterDir == 0x00 || filterDir == filterMask) 
+            {
+                // motion away from the average - reset the average
+                filterDir = 0x5555;
+                filterN = 1;
+                filterSum = (lastAvg + z)/2;
+                return int16_t(filterSum);
+            }
+            else
+            {
+                // we're directionless - return the new average, with the 
+                // new sample included
+                filterSum += z;
+                ++filterN;
+                return int16_t(filterSum / filterN);
+            }
+        }
+        else
+        {
+            // firing mode - skip the filter
+            filterN = 1;
+            filterSum = z;
+            filterDir = 0x5555;
+            return z;
+        }
+    }
+#elif PLUNGER_FILTERING_MODE == 2
     // Apply pre-processing filter.  This filter is applied to the raw
     // value coming off the sensor, before calibration or fire-event
     // processing.
@@ -3725,69 +3963,6 @@
     {
         return z;
     }
-#else
-    // Apply pre-processing filter.  This filter is applied to the raw
-    // value coming off the sensor, before calibration or fire-event
-    // processing.
-    void applyPreFilter(PlungerReading &r)
-    {
-    }
-    
-    // Figure the next post-processing filtered value.  This applies a
-    // hysteresis filter to the last raw z value and returns the 
-    // filtered result.
-    int applyPostFilter()
-    { 
-        if (firing <= 1)
-        {
-            // Filter limit - 5 samples.  Once we've been moving
-            // in the same direction for this many samples, we'll
-            // clear the history and start over.
-            const int filterMask = 0x1f;
-            
-            // figure the last average
-            int lastAvg = int(filterSum / filterN);
-            
-            // figure the direction of this sample relative to the average,
-            // and shift it in to our bit mask of recent direction data
-            if (z != lastAvg)
-            {
-                // shift the new direction bit into the vector
-                filterDir <<= 1;
-                if (z > lastAvg) filterDir |= 1;
-            }
-            
-            // keep only the last N readings, up to the filter limit
-            filterDir &= filterMask;
-            
-            // if we've been moving consistently in one direction (all 1's
-            // or all 0's in the direction history vector), reset the average
-            if (filterDir == 0x00 || filterDir == filterMask) 
-            {
-                // motion away from the average - reset the average
-                filterDir = 0x5555;
-                filterN = 1;
-                filterSum = (lastAvg + z)/2;
-                return int16_t(filterSum);
-            }
-            else
-            {
-                // we're directionless - return the new average, with the 
-                // new sample included
-                filterSum += z;
-                ++filterN;
-                return int16_t(filterSum / filterN);
-            }
-        }
-        else
-        {
-            // firing mode - skip the filter
-            filterN = 1;
-            filterSum = z;
-            filterDir = 0x5555;
-            return z;
-        }
-    }
 #endif
     
     void initFilter()
@@ -4199,7 +4374,7 @@
 #define v_byte(var, ofs)    data[ofs] = cfg.var
 #define v_ui16(var, ofs)    ui16Wire(data+(ofs), cfg.var)
 #define v_pin(var, ofs)     pinNameWire(data+(ofs), cfg.var)
-#define v_byte_ro(val, ofs) data[ofs] = val
+#define v_byte_ro(val, ofs) data[ofs] = (val)
 #define v_func  configVarGet
 #include "cfgVarMsgMap.h"
 
@@ -4241,24 +4416,23 @@
         ledWizMode = true;
 
         // update all on/off states
-        for (int i = 0, bit = 1, ri = 1 ; i < numLwOutputs ; ++i, bit <<= 1)
+        for (int i = 0, bit = 1, imsg = 1, iwiz = ledWizBank*32 ; 
+             i < 32 && iwiz < numOutputs ;
+             ++i, ++iwiz, bit <<= 1)
         {
             // figure the on/off state bit for this output
             if (bit == 0x100) {
                 bit = 1;
-                ++ri;
+                ++imsg;
             }
             
             // set the on/off state
-            wizOn[i] = ((data[ri] & bit) != 0);
+            wizOn[iwiz] = ((data[imsg] & bit) != 0);
         }
         
         // set the flash speed - enforce the value range 1-7
-        wizSpeed = data[5];
-        if (wizSpeed < 1)
-            wizSpeed = 1;
-        else if (wizSpeed > 7)
-            wizSpeed = 7;
+        if (ledWizBank < countof(wizSpeed))
+            wizSpeed[ledWizBank] = (data[5] < 1 ? 1 : data[5] > 7 ? 7 : data[5]);
 
         // update the physical outputs
         updateWizOuts();
@@ -4337,7 +4511,7 @@
                 numOutputs, 
                 cfg.psUnitNo - 1,   // report 0-15 range for unit number (we store 1-16 internally)
                 cfg.plunger.cal.zero, cfg.plunger.cal.max, cfg.plunger.cal.tRelease,
-                nvm.valid());
+                nvm.valid(), xmalloc_rem);
             break;
             
         case 5:
@@ -4391,6 +4565,26 @@
             // 10 = Build ID query.
             js.reportBuildInfo(getBuildID());
             break;
+            
+        case 11:
+            // 11 = TV ON relay control.
+            //      data[2] = operation:
+            //         0 = turn relay off
+            //         1 = turn relay on
+            //         2 = pulse relay (as though the power-on timer fired)
+            TVRelay(data[2]);
+            break;
+            
+        case 12:
+            // 12 = Select virtual LedWiz unit.  This selects a bank of 32
+            // outputs for subsequent SBA and PBA messages.
+            ledWizBank = data[2];
+            break;
+            
+        case 13:
+            // 13 = Send button status report
+            reportButtonStatus(js);
+            break;
         }
     }
     else if (data[0] == 66)
@@ -4471,15 +4665,17 @@
         // flag that we received an LedWiz message
         ledWizMode = true;
 
-        // Update all output profile settings
-        for (int i = 0 ; i < 8 ; ++i)
-            wizVal[pbaIdx + i] = data[i];
+        // Update all output profile settings for the current bank
+        for (int i = 0, iwiz = ledWizBank*32 + pbaIdx ; 
+             i < 8 && iwiz < numOutputs ; 
+             ++i, ++iwiz)
+            wizVal[iwiz] = 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)
+        if (pbaIdx >= 24)
         {
             updateWizOuts();
             if (hc595 != 0)
@@ -4817,7 +5013,8 @@
         // figure the current status flags for joystick reports
         uint16_t statusFlags =
             (cfg.plunger.enabled ? 0x01 : 0x00)
-            | (nightMode ? 0x02 : 0x00);
+            | (nightMode ? 0x02 : 0x00)
+            | ((psu2_state & 0x07) << 2);
 
         // If it's been long enough since our last USB status report, send
         // the new report.  VP only polls for input in 10ms intervals, so
@@ -5040,6 +5237,12 @@
                     jsOKTimer.stop();
                 }
             }
+            else if (psu2_state >= 4)
+            {
+                // We're in the TV timer countdown.  Skip the normal heartbeat
+                // flashes and show the TV timer flashes instead.
+                diagLED(0, 0, 0);
+            }
             else if (cfg.plunger.enabled && !cfg.plunger.cal.calibrated)
             {
                 // connected, plunger calibration needed - flash yellow/green