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

Dependencies:   mbed FastIO FastPWM USBDevice

Fork of Pinscape_Controller by Mike R

/media/uploads/mjr/pinscape_no_background_small_L7Miwr6.jpg

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

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

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

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

Downloads

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

Documentation

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

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

System Requirements

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

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

Main Features

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

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

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

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

Expansion Boards

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

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

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

Expansion Board project page

Update notes

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

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

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

New Features

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

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

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

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

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

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

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

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

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

More Downloads

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

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

Copyright and License

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

Warning to VirtuaPin Kit Owners

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

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

Files at this revision

API Documentation at this revision

Comitter:
mjr
Date:
Tue May 09 05:48:37 2017 +0000
Parent:
86:e30a1f60f783
Child:
88:98bce687e6c0
Commit message:
AEDR-8300, VL6180X, TLC59116; new plunger firing detection

Changed in this revision

BitBangI2C/BitBangI2C.cpp Show annotated file Show diff for this revision Revisions of this file
BitBangI2C/BitBangI2C.h Show annotated file Show diff for this revision Revisions of this file
Plunger/barCodeSensor.h Show annotated file Show diff for this revision Revisions of this file
Plunger/distanceSensor.h Show annotated file Show diff for this revision Revisions of this file
Plunger/edgeSensor.h Show annotated file Show diff for this revision Revisions of this file
Plunger/plunger.h Show annotated file Show diff for this revision Revisions of this file
Plunger/tsl14xxSensor.h Show annotated file Show diff for this revision Revisions of this file
TLC59116/TLC59116.h 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
TSL14xx/TSL14xx.h Show annotated file Show diff for this revision Revisions of this file
USBJoystick/USBJoystick.cpp Show annotated file Show diff for this revision Revisions of this file
USBJoystick/USBJoystick.h Show annotated file Show diff for this revision Revisions of this file
USBProtocol.h Show annotated file Show diff for this revision Revisions of this file
VL6180X/VL6180X.cpp Show annotated file Show diff for this revision Revisions of this file
VL6180X/VL6180X.h Show annotated file Show diff for this revision Revisions of this file
cfgVarMsgMap.h Show annotated file Show diff for this revision Revisions of this file
config.h Show annotated file Show diff for this revision Revisions of this file
main.cpp Show annotated file Show diff for this revision Revisions of this file
--- a/BitBangI2C/BitBangI2C.cpp	Fri Apr 21 18:50:37 2017 +0000
+++ b/BitBangI2C/BitBangI2C.cpp	Tue May 09 05:48:37 2017 +0000
@@ -16,7 +16,7 @@
 // dprintf() = general debug diagnostics (printed only in case 2)
 // eprintf() = error diagnostics (printed in case 1 and above)
 //
-#define BBI2C_DEBUG 1
+#define BBI2C_DEBUG 0
 #if BBI2C_DEBUG
 # define eprintf(...) printf(__VA_ARGS__)
 # if BBI2C_DEBUG >= 2
@@ -46,11 +46,14 @@
 //
 // Bit-bang I2C implementation
 //
-BitBangI2C::BitBangI2C(PinName sda, PinName scl) :
-    sclPin(scl), sdaPin(sda)
+BitBangI2C::BitBangI2C(PinName sda, PinName scl, bool internalPullup) :
+    sdaPin(sda, internalPullup), sclPin(scl, internalPullup)
 {
     // set the default frequency to 100kHz
     frequency(100000);
+    
+    // we're initially in a stop
+    inStop = true;
 }
 
 void BitBangI2C::frequency(uint32_t freq)
@@ -62,72 +65,101 @@
     if (freq <= 100000)
     {
         // standard mode I2C bus - up to 100kHz
+        
+        // nanosecond parameters
         tLow = calcHiResWaitTime(4700);
         tHigh = calcHiResWaitTime(4000);
-        tBuf = calcHiResWaitTime(4700);
         tHdSta = calcHiResWaitTime(4000);
         tSuSta = calcHiResWaitTime(4700);
         tSuSto = calcHiResWaitTime(4000);
         tAck = calcHiResWaitTime(300);
-        tData = calcHiResWaitTime(300);
         tSuDat = calcHiResWaitTime(250);
+        tBuf = calcHiResWaitTime(4700);
     }
     else if (freq <= 400000)
     {
         // fast mode I2C - up to 400kHz
+
+        // nanosecond parameters
         tLow = calcHiResWaitTime(1300);
         tHigh = calcHiResWaitTime(600);
-        tBuf = calcHiResWaitTime(1300);
         tHdSta = calcHiResWaitTime(600);
         tSuSta = calcHiResWaitTime(600);
         tSuSto = calcHiResWaitTime(600);
         tAck = calcHiResWaitTime(100);
-        tData = calcHiResWaitTime(100);
         tSuDat = calcHiResWaitTime(100);
+        tBuf = calcHiResWaitTime(1300);
     }
     else
     {
         // fast mode plus - up to 1MHz
+
+        // nanosecond parameters
         tLow = calcHiResWaitTime(500);
         tHigh = calcHiResWaitTime(260);
-        tBuf = calcHiResWaitTime(500);
         tHdSta = calcHiResWaitTime(260);
         tSuSta = calcHiResWaitTime(260);
         tSuSto = calcHiResWaitTime(260);
         tAck = calcHiResWaitTime(50);
-        tData = calcHiResWaitTime(50);
         tSuDat = calcHiResWaitTime(50);
+        tBuf = calcHiResWaitTime(500);
     }
 }
 
 void BitBangI2C::start() 
 {    
-    // take clock and data high
-    sclHi();
-    sdaHi();
-    hiResWait(tBuf);
+    // check to see if we're starting after a stop, or if this is a
+    // repeated start
+    if (inStop)
+    {
+        // in a stop - make sure we waited for the minimum hold time
+        hiResWait(tBuf);
+    }
+    else
+    {
+        // repeated start - take data high
+        sdaHi();
+        hiResWait(tSuDat);
+        
+        // take clock high
+        sclHi();
+        
+        // wait for the minimum setup period
+        hiResWait(tSuSta);
+    }
     
     // take data low
     sdaLo();
-    hiResWait(tHdSta);
     
-    // take clock low
+    // wait for the setup period and take clock low
+    hiResWait(tHdSta);
     sclLo();
-    hiResWait(tLow);
+
+    // wait for the low period
+    hiResWait(tLow);    
+    
+    // no longer in a stop
+    inStop = false;
 }
 
 void BitBangI2C::stop() 
 {
-    // take SDA low
-    sdaLo();
+    // if we're not in a stop, enter one
+    if (!inStop)
+    {
+        // take SDA low
+        sdaLo();
 
-    // take SCL high
-    sclHi();
-    hiResWait(tSuSto);
-    
-    // take SDA high
-    sdaHi();
-    hiResWait(tBuf);
+        // take SCL high
+        sclHi();
+        hiResWait(tSuSto);
+
+        // take SDA high
+        sdaHi();
+        
+        // we're in a stop
+        inStop = true;
+    }
 }
 
 bool BitBangI2C::wait(uint32_t timeout_us)
@@ -233,13 +265,25 @@
     // write the bits, most significant first
     for (int i = 0 ; i < 8 ; ++i, data <<= 1)
         writeBit(data & 0x80);
-
-    // read and return the ACK bit
-    return readBit();
+        
+    // release SDA so the device can control it
+    sdaHi();
+        
+    // read the ACK bit
+    int ack = readBit();
+    
+    // take SDA low again
+    sdaLo();
+            
+    // return success if ACK was 0
+    return ack;
 }
 
 int BitBangI2C::read(bool ack) 
 {
+    // take SDA high before reading
+    sdaHi();
+
     // read 8 bits, most significant first
     uint8_t data = 0;
     for (int i = 0 ; i < 8 ; ++i)
@@ -248,6 +292,9 @@
     // switch to output mode and send the ACK bit
     writeBit(!ack);
 
+    // release SDA
+    sdaHi();
+
     // return the data byte we read
     return data;
 }
@@ -260,27 +307,29 @@
     // Wait (within reason) for it to actually read as high.  The device
     // can intentionally pull the clock line low to tell us to wait while
     // it's working on preparing the data for us.
-    Timer t;
-    t.start();
-    while (sclPin.read() == 0 && t.read_us() < 500000) ;
-    
-    // if the clock isn't high, we timed out
-    if (sclPin.read() == 0)
+    int t = 0;
+    do
     {
-        eprintf("i2c.readBit, clock stretching timeout\r\n");
-        return 0;
+        // if the clock is high, we're ready to go
+        if (sclPin.read())
+        {
+            // wait for the data setup time
+            hiResWait(tSuDat);
+            
+            // read the bit    
+            bool bit = sdaPin.read();
+            
+            // take the clock low again
+            sclLo();
+            hiResWait(tLow);
+            
+            // return the bit
+            return bit;
+        }
     }
-    
-    // wait until the clock interval is up
-    while (t.read_us() < clkPeriod_us);
-    
-    // read the bit    
-    bool bit = sdaPin.read();
-    
-    // take the clock low again
-    sclLo();
-    hiResWait(tLow);
-    
-    // return the bit
-    return bit;
+    while (t++ < 100000);
+
+    // we timed out
+    eprintf("i2c.readBit, clock stretching timeout\r\n");
+    return 0;
 }
--- a/BitBangI2C/BitBangI2C.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/BitBangI2C/BitBangI2C.h	Tue May 09 05:48:37 2017 +0000
@@ -34,13 +34,17 @@
 // the nominal address from the data sheet left one bit in each call
 // to a routine here.
 //
-// Electrically, the I2C bus is designed as a a pair of open-collector 
-// lines with pull-up resistors.  Any device can pull a line low by
-// shorting it to ground, but no one can pull a line high: instead, you
-// *allow* a line to go high by releasing it, which is to say putting
-// your connection to it in a high-Z state.  On an MCU, we put a GPIO
-// pin in high-Z state by setting its direction to INPUT mode.  So our
-// GPIO write strategy is like this:
+// Electrically, the I2C bus consists of two lines, SDA (data) and SCL
+// (clock).  Multiple devices can connect to the bus by connecting to
+// these two lines; the lines are shared among all of the devices.  Each
+// line has a pull-up resistor that pulls it to logic '1' voltage.  Each
+// device connects with an open-collector circuit that can short the line
+// to ground (logic '0').  This means that any device can assert a 'low'
+// but no one can actually assert a 'high'; the pull-up makes it so that
+// a 'high' occurs when no one is asserting a 'low'.  On an MCU, we release
+// a line by putting the GPIO pin in high-Z state, which we can do on the
+// KL25Z by setting its direction to INPUT mode.  So our GPIO write strategy 
+// is like this:
 //
 //   - take a pin low (0):   
 //        pin.input(); 
@@ -54,6 +58,12 @@
 // the direction to output is enough to assert the low level, since the
 // hardware asserts the level that was previously stored in the output
 // register whenever the direction is changed from input to output.
+//
+// The KL25Z by default provides a built-in pull-up resistor on each GPIO
+// set to input mode.  This can optionally be used as the bus-wide pull-up
+// for each line.  Standard practice is to use external pull-up resistors
+// rather than MCU pull-ups, but the internal pull-ups are fine for ad hoc
+// setups where there's only one external device connected to a GPIO pair.
 
 
 #ifndef _BITBANGI2C_H_
@@ -64,6 +74,27 @@
 #include "pinmap.h"
 
 
+// For testing purposes: a cover class for the mbed library I2C bridging
+// the minor differences in our interface.  This allows switching between
+// BitBangI2C and the mbed library I2C via a macro of the like.
+class MbedI2C: public I2C
+{
+public:
+    MbedI2C(PinName sda, PinName scl, bool internalPullups) : I2C(sda, scl) { }
+    
+    int write(int addr, const uint8_t *data, size_t len, bool repeated = false)
+    {
+        return I2C::write(addr, (const char *)data, len, repeated);
+    }
+    int read(int addr, uint8_t *data, size_t len, bool repeated = false)
+    {
+        return I2C::read(addr, (char *)data, len, repeated);
+    }
+    
+    void reset() { }
+};
+
+
 // DigitalInOut replacmement class for I2C use.  I2C uses pins a little
 // differently from other use cases.  I2C is a bus, where many devices can
 // be attached to each line.  To allow this shared access, devices can 
@@ -81,19 +112,23 @@
 class I2CInOut
 {
 public:
-    I2CInOut(PinName pin)
+    I2CInOut(PinName pin, bool internalPullup)
     {
         // initialize the pin
         gpio_t g;
         gpio_init(&g, pin);
         
         // get the registers
-        unsigned int port = (unsigned int)pin >> PORT_SHIFT;
-        FGPIO_Type *r = (FGPIO_Type *)(FPTA_BASE + port*0x40);
-        __IO uint32_t *pin_pcr = (__IO uint32_t*)(PORTA_BASE + pin); 
+        unsigned int portno = (unsigned int)pin >> PORT_SHIFT;
+        uint32_t pinno = (uint32_t)(pin & 0x7C) >> 2;
+        FGPIO_Type *r = (FGPIO_Type *)(FPTA_BASE + portno*0x40);
+        __IO uint32_t *pin_pcr = &(((PORT_Type *)(PORTA_BASE + 0x1000*portno)))->PCR[pinno];
         
-        // set no-pull-up mode (clear PE bit = Pull Enable)
-        *pin_pcr &= ~0x02;
+        // set the desired internal pull-up mode
+        if (internalPullup)
+            *pin_pcr |= 0x02;
+        else
+            *pin_pcr &= ~0x02;
            
         // save the register information we'll need later
         this->mask = g.mask;
@@ -148,7 +183,7 @@
 {
 public:    
     // create the interface
-    BitBangI2C(PinName sda, PinName scl);
+    BitBangI2C(PinName sda, PinName scl, bool internalPullups);
 
     // set the bus frequency in Hz
     void frequency(uint32_t freq);
@@ -192,7 +227,7 @@
         
         // clock it
         sclPin.hi();
-        hiResWait(tData);
+        hiResWait(tHigh);
         
         // drop the clock
         sclPin.lo();
@@ -208,9 +243,9 @@
     inline void sdaHi() { sdaPin.hi(); }
     inline void sdaLo() { sdaPin.lo(); }
 
-    // SCL and SDA pins
+    // SDA and SCL pins
+    I2CInOut sdaPin;
     I2CInOut sclPin;
-    I2CInOut sdaPin;
 
     // inverse of frequency = clock period in microseconds
     uint32_t clkPeriod_us;
@@ -263,13 +298,15 @@
     //
     int tLow;       // SCL low period
     int tHigh;      // SCL high period
-    int tBuf;       // bus free time between start and stop conditions
     int tHdSta;     // hold time for start condition
     int tSuSta;     // setup time for repeated start condition
     int tSuSto;     // setup time for stop condition
     int tSuDat;     // data setup time
     int tAck;       // ACK time
-    int tData;      // data valid time
+    int tBuf;       // bus free time between start and stop conditions
+    
+    // are we in a Stop condition?
+    bool inStop;
 };
  
 #endif /* _BITBANGI2C_H_ */
--- a/Plunger/barCodeSensor.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/barCodeSensor.h	Tue May 09 05:48:37 2017 +0000
@@ -5,10 +5,19 @@
 // by reading the bar code and decoding it into a position figure.
 //
 // The bar code has to be encoded in a specific format that we recognize.
-// We use a 10-bit reflected Gray code, optically encoded using a Manchester-
-// type of coding.  Each bit is represented as a fixed-width area on the
-// bar, half white and half black.  The bit value is encoded in the order
-// of the colors: Black/White is '0', and White/Black is '1'.
+// We use a reflected Gray code, optically encoded in black/white pixel
+// patterns.  Each bit is represented by a fixed-width area.  Half the
+// pixels in every bit are white, and half are black.  A '0' bit is
+// represented by black pixels in the left half and white pixels in the
+// right half, and a '1' bit is white on the left and black on the right.
+// To read a bit, we identify the set of pixels covering the bit's fixed
+// area in the code, then we see if the left or right half is brighter.
+//
+// (This optical encoding scheme is based on Manchester coding, which is 
+// normally used in the context of serial protocols, but translates to 
+// bar codes straightforwardly.  Replace the serial protocol's time
+// dimension with the spatial dimension across the bar, and replace the
+// high/low wire voltage levels with white/black pixels.)
 //
 // Gray codes are ideal for this type of application.  Gray codes are
 // defined such that each code point differs in exactly one bit from each
@@ -20,21 +29,73 @@
 // the reading will come out as one of points on either side of the true
 // position.  Finally, motion blur will have the same effect, of creating
 // ambiguity in the least significant bits, and thus giving us a reading
-// that's correct to as many bits as we can read with teh blur.
+// that's correct to as many bits as we can make out.
 //
-// We use the Manchester-type optical coding because it has good properties
-// for low-contrast images, and doesn't require uniform lighting.  Each bit's
-// pixel span contains equal numbers of light and dark pixels, so each bit
-// provides its own local level reference.  This means we don't care about
-// lighting uniformity over the whole image, because we don't need a global
-// notion of light and dark, just a local one over a single bit at a time.
+// The half-and-half optical coding also has good properties for our
+// purposes.  The fixed-width bit regions require essentially no CPU work
+// to find the bits, which is good because we're using a fairly slow CPU.
+// The half white/half black coding of each pixel makes every pixel 
+// self-relative in terms of brightness, so we don't need to figure the 
+// black and white thresholds globally for the whole image.  That makes 
+// the physical device engineering and installation easier because the 
+// software can tolerate a fairly wide range of lighting conditions.
 // 
 
 #ifndef _BARCODESENSOR_H_
 #define _BARCODESENSOR_H_
 
 #include "plunger.h"
-#include "tsl14xxSensor.h"
+
+// Gray code to binary mapping for our special coding.  This is a custom
+// 7-bit code, minimum run length 6, 110 positions populated.  The minimum
+// run length is the minimum number of consecutive code points where each
+// bit must remain fixed.  For out optical coding, this defines the smallest
+// "island" size for a black or white bar horizontally.  Small features are
+// prone to light scattering that makes them appear gray on the sensor.
+// Larger features are less subject to scatter, making them easier to 
+// distinguish by brightness level.
+static const uint8_t grayToBin[] = {
+   0,   1,  83,   2,  71, 100,  84,   3,  69, 102,  82, 128,  70, 101,  57,   4,    // 0-15
+  35,  50,  36,  37,  86,  87,  85, 128,  34, 103,  21, 104, 128, 128,  20,   5,    // 16-31
+  11, 128,  24,  25,  98,  99,  97,  40,  68,  67,  81,  80,  55,  54,  56,  41,    // 32-47
+  10,  51,  23,  38, 128,  52, 128,  39,   9,  66,  22, 128,   8,  53,   7,   6,    // 48-63
+  47,  14,  60, 128,  72,  15,  59,  16,  46,  91,  93,  92,  45, 128,  58,  17,    // 64-79
+  48,  49,  61,  62,  73,  88,  74,  75,  33,  90, 106, 105,  32,  89,  19,  18,    // 80-95
+  12,  13,  95,  26, 128,  28,  96,  27, 128, 128,  94,  79,  44,  29,  43,  42,    // 96-111
+ 128,  64, 128,  63, 110, 128, 109,  76, 128,  65, 107,  78,  31,  30, 108,  77     // 112-127
+};
+
+
+// Auto-exposure counter
+class BarCodeExposureCounter
+{
+public:
+    BarCodeExposureCounter()
+    {
+        nDark = 0;
+        nBright = 0;
+        nZero = 0;
+        nSat = 0;
+    }
+    
+    inline void count(int pix)
+    {
+        if (pix <= 2)
+            ++nZero;
+        else if (pix < 12)
+            ++nDark;
+        else if (pix >= 253)
+            ++nSat;
+        else if (pix > 200)
+            ++nBright;
+    }
+    
+    int nDark;      // dark pixels
+    int nBright;    // bright pixels
+    int nZero;      // pixels at zero brightness
+    int nSat;       // pixels at full saturation
+};
+
 
 // Base class for bar-code sensors
 //
@@ -58,13 +119,51 @@
 // full bit, including both "half bits" - it's the full white/black or 
 // black/white pattern area.
 
+struct BarCodeProcessResult
+{
+    int pixofs;
+    int raw;
+    int mask;
+};
+
 template <int nBits, int leftBarWidth, int leftBarMaxOfs, int bitWidth>
-class PlungerSensorBarCode
+class PlungerSensorBarCode: public PlungerSensorImage<BarCodeProcessResult>
 {
 public:
+    PlungerSensorBarCode(PlungerSensorImageInterface &sensor, int npix) 
+        : PlungerSensorImage(sensor, npix, (1 << nBits) - 1)
+    {
+        startOfs = 0;
+    }
+
+    // process a configuration change
+    virtual void onConfigChange(int varno, Config &cfg)
+    {
+        // check for bar-code variables
+        switch (varno)
+        {
+        case 20:
+            // bar code offset
+            startOfs = cfg.plunger.barCode.startPix;
+            break;
+        }
+        
+        // do the generic work
+        PlungerSensorImage::onConfigChange(varno, cfg);
+    }
+
+protected:
     // process the image    
-    bool process(const uint8_t *pix, int npix, int &pos)
+    virtual bool process(const uint8_t *pix, int npix, int &pos, BarCodeProcessResult &res)
     {
+        // adjust auto-exposure
+        adjustExposure(pix, npix);
+        
+        // clear the result descriptor
+        res.pixofs = 0;
+        res.raw = 0;
+        res.mask = 0;
+        
 #if 0 // $$$
 
         // scan from the left edge until we find the fixed '0' start bit
@@ -147,25 +246,46 @@
         }
         else
         {
-            barStart = 4; // $$$ should be configurable via config tool
+            // start at the fixed pixel offset
+            barStart = startOfs;
         }
 
+        // Start with zero in the barcode and success mask.  The mask
+        // indicates which bits we were able to read successfully: a
+        // '1' bit in the mask indicates that the corresponding bit
+        // position in 'barcode' was successfully read, a '0' bit means
+        // that the image was too fuzzy to read.
+        int barcode = 0, mask = 0;
+
         // Scan the bits
-        int barcode = 0;
         for (int bit = 0, x0 = barStart; bit < nBits ; ++bit, x0 += bitWidth)
         {
-            // figure the extent of this bit
-            int x1 = x0 + bitWidth / 2;
-            int x2 = x0 + bitWidth;
+#if 0
+            // Figure the extent of this bit.  The last bit is double
+            // the width of the other bits, to give us a better chance
+            // of making out the small features of the last bit.
+            int w = bitWidth;
+            if (bit == nBits - 1) w *= 2;
+#else
+            // width of the bit
+            const int w = bitWidth;
+#endif
+
+            // figure the bit's internal pixel layout
+            int halfBitWidth = w / 2;
+            int x1 = x0 + halfBitWidth;     // midpoint
+            int x2 = x0 + w;                // right edge
+            
+            // make sure we didn't go out of bounds
             if (x1 > npix) x1 = npix;
             if (x2 > npix) x2 = npix;
 
+#if 0
             // get the average of the pixels over the bit
             int sum = 0;
             for (int x = x0 ; x < x2 ; ++x)
                 sum += pix[x];
-            int avg = sum / bitWidth;
-
+            int avg = sum / w;
             // Scan the left and right sections.  Classify each
             // section according to whether the majority of its
             // pixels are above or below the local average.
@@ -174,20 +294,56 @@
                 lsum += (pix[x] < avg ? 0 : 1);
             for (int x = x1 + 1 ; x < x2 - 1 ; ++x)
                 rsum += (pix[x] < avg ? 0 : 1);
+#else
+            // Sum the pixel readings in each half-bit.  Ignore
+            // the first and last bit of each section, since these
+            // could be contaminated with scattered light from the
+            // adjacent half-bit.  On the right half, hew to the 
+            // right side if the overall pixel width is odd. 
+            int lsum = 0, rsum = 0;
+            for (int x = x0 + 1 ; x < x1 - 1 ; ++x)
+                lsum += pix[x];
+            for (int x = x2 - halfBitWidth + 1 ; x < x2 - 1 ; ++x)
+                rsum += pix[x];
+#endif
                 
-            // if we don't have a winner, fail
-            if (lsum == rsum)
-                return false;
+            // shift a zero bit into the code and success mask
+            barcode <<= 1;
+            mask <<= 1;
 
-            // black/white = 0, white/black = 1
-            barcode = (barcode << 1) | (lsum < rsum ? 0 : 1);
+            // Brightness difference required per pixel.  Higher values
+            // require greater contrast to make a reading, which reduces
+            // spurious readings at the cost of reducing the overall 
+            // success rate.  The right level depends on the quality of
+            // the optical system.  Setting this to zero makes us maximally
+            // tolerant of low-contrast images, allowing for the simplest
+            // optical system.  Our simple optical system suffers from
+            // poor focus, which in turn causes poor contrast in small
+            // features.
+            const int minDelta = 2;
+
+            // see if we could tell the difference in brightness
+            int delta = lsum - rsum;
+            if (delta < 0) delta = -delta;
+            if (delta > minDelta * w/2)
+            {
+                // got it - black/white = 0, white/black = 1
+                if (lsum > rsum) barcode |= 1;
+                mask |= 1;
+            }
         }
 
         // decode the Gray code value to binary
-        pos = grayToBin(barcode);
+        pos = grayToBin[barcode];
         
-        // success
-        return true;
+        // set the results descriptor structure
+        res.pixofs = barStart;
+        res.raw = barcode;
+        res.mask = mask;
+    
+        // return success if we decoded all bits, and the Gray-to-binary
+        // mapping was populated
+        return pos != (1 << nBits) && mask == ((1 << nBits) - 1);
 #endif
     }
     
@@ -255,83 +411,17 @@
         }
     }
 
-    // convert a reflected Gray code value (up to 16 bits) to binary
-    int grayToBin(int grayval)
-    {
-        int temp = grayval ^ (grayval >> 8);
-        temp ^= (temp >> 4);
-        temp ^= (temp >> 2);
-        temp ^= (temp >> 1);
-        return temp;
-    }
-};
-
-// Auto-exposure counter
-class BarCodeExposureCounter
-{
-public:
-    BarCodeExposureCounter()
-    {
-        nDark = 0;
-        nBright = 0;
-        nZero = 0;
-        nSat = 0;
-    }
-    
-    inline void count(int pix)
-    {
-        if (pix <= 2)
-            ++nZero;
-        else if (pix < 12)
-            ++nDark;
-        else if (pix >= 253)
-            ++nSat;
-        else if (pix > 200)
-            ++nBright;
-    }
-    
-    int nDark;      // dark pixels
-    int nBright;    // bright pixels
-    int nZero;      // pixels at zero brightness
-    int nSat;       // pixels at full saturation
-};
-
-// PlungerSensor interface implementation for bar code readers.
-//
-// Bar code readers are image sensors, so we have a pixel size for
-// the sensor.  However, this isn't the scale for the readings.  The
-// scale for the readings is determined by the number of bits in the
-// bar code, since an n-bit bar code can encode 2^n distinct positions.
-//
-template <int nBits, int leftBarWidth, int leftBarMaxOfs, int bitWidth>
-class PlungerSensorBarCodeTSL14xx: public PlungerSensorTSL14xxSmall,
-    PlungerSensorBarCode<nBits, leftBarWidth, leftBarMaxOfs, bitWidth>
-{
-public:
-    PlungerSensorBarCodeTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
-        : PlungerSensorTSL14xxSmall(nativePix, (1 << nBits) - 1, si, clock, ao)
-    {
-        // the native scale is the number of positions we can
-        // encode in the bar code
-        nativeScale = 1023;
-    }
-    
-protected:
-    
-    // process the image through the bar code reader
-    virtual bool process(const uint8_t *pix, int npix, int &pos)
-    {
-        // adjust the exposure
-        adjustExposure(pix, npix);
-        
-        // do the standard bar code processing
-        return PlungerSensorBarCode<nBits, leftBarWidth, leftBarMaxOfs, bitWidth>
-            ::process(pix, npix, pos);
-    }
-    
     // bar code sensor orientation is fixed
     virtual int getOrientation() const { return 1; }
     
+    // send extra status report headers
+    virtual void extraStatusHeaders(USBJoystick &js, BarCodeProcessResult &res)
+    {
+        // Send the bar code status report.  We use coding type 1 (Gray code,
+        // Manchester pixel coding).
+        js.sendPlungerStatusBarcode(nBits, 1, res.pixofs, bitWidth, res.raw, res.mask);
+    }
+    
     // adjust the exposure
     void adjustExposure(const uint8_t *pix, int npix)
     {
@@ -342,23 +432,24 @@
         // pixels has to look: the bit area will be 50% black and 50%
         // white, and the margins will be all white.  For maximum
         // contrast, target an exposure level where the black pixels
-        // are all below the middle brightness level and the white
+        // are all below a certain brightness level and the white
         // pixels are all above.  Start by figuring the number of
         // pixels above and below.
-        int nDark = 0;
+        const int medianTarget = 160;
+        int nBelow = 0;
         for (int i = 0 ; i < npix ; ++i)
         {
-            if (pix[i] < 200)
-                ++nDark;
+            if (pix[i] < medianTarget)
+                ++nBelow;
         }
         
-        // Figure the percentage of black pixels: the left bar is
+        // Figure the desired number of black pixels: the left bar is
         // all black pixels, and 50% of each bit is black pixels.
-        int targetDark = leftBarWidth + (nBits * bitWidth)/2;
+        int targetBelow = leftBarWidth + (nBits * bitWidth)/2;
         
         // Increase exposure time if too many pixels are below the
         // halfway point; decrease it if too many pixels are above.
-        int d = nDark - targetDark;
+        int d = nBelow - targetBelow;
         if (d > 5 || d < -5)
         {
             axcTime += d;
@@ -475,20 +566,21 @@
         if (axcTime > 2500)
             axcTime = 2500;
     }
-};
 
-// TSL1401CL - 128-bit image sensor, used as a bar code reader
-class PlungerSensorTSL1401CL: public PlungerSensorBarCodeTSL14xx<
-    10,  // number of bits in code
-    0,   // left delimiter bar width in pixels (0 for none)
-    24,  // maximum left margin width in pixels
-    12>  // pixel width of each bit
-{
-public:
-    PlungerSensorTSL1401CL(PinName si, PinName clock, PinName a0)
-        : PlungerSensorBarCodeTSL14xx(128, si, clock, a0)
+#if 0
+    // convert a reflected Gray code value (up to 16 bits) to binary
+    static inline int grayToBin(int grayval)
     {
+        int temp = grayval ^ (grayval >> 8);
+        temp ^= (temp >> 4);
+        temp ^= (temp >> 2);
+        temp ^= (temp >> 1);
+        return temp;
     }
+#endif
+
+    // bar code starting pixel offset
+    int startOfs;
 };
 
 #endif
--- a/Plunger/distanceSensor.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/distanceSensor.h	Tue May 09 05:48:37 2017 +0000
@@ -43,8 +43,8 @@
 public:
     PlungerSensorDistance(int nativeScale) : PlungerSensor(nativeScale)
     {
-        // start the sample timer
-        t.start();
+        totalTime = 0;
+        nRuns = 0;
     }
 
     // get the average scan time
@@ -58,11 +58,7 @@
         nRuns += 1;
     }
 
-    // sample timer
-    Timer t;
-    
     // scan time statistics
-    uint32_t tStart;          // time (on this->t) of start of current scan
     uint64_t totalTime;       // total time consumed by all reads so far
     uint32_t nRuns;           // number of runs so far
 };
@@ -73,66 +69,79 @@
 // sensor units are millimeters.  A physical plunger has about 3" of
 // total travel, but leave a little extra padding for measurement
 // inaccuracies and other unusual situations, so'll use an actual
-// native scale of 5" = 127mm.
+// native scale of 150mm.
 class PlungerSensorVL6180X: public PlungerSensorDistance
 {
 public:
     PlungerSensorVL6180X(PinName sda, PinName scl, PinName gpio0)
-        : PlungerSensorDistance(127),
-          sensor(sda, scl, I2C_ADDRESS, gpio0)
+        : PlungerSensorDistance(150),
+          sensor(sda, scl, I2C_ADDRESS, gpio0, true)
     {
     }
     
-    static const int I2C_ADDRESS = 0x28;
+    // fixed I2C bus address for the VL6180X
+    static const int I2C_ADDRESS = 0x29;
     
     virtual void init()
     {
-        // reboot and initialize the sensor
+        // initialize the sensor and set the default configuration
         sensor.init();
-        
-        // set the default configuration
         sensor.setDefaults();
         
-        // start the first reading
-        tStart = t.read_us();
+        // start a reading
         sensor.startRangeReading();
     }
     
     virtual bool ready()
     {
+        // make sure a reading has been initiated
+        sensor.startRangeReading();
+        
+        // check if a reading is ready
         return sensor.rangeReady();
     }
     
     virtual bool readRaw(PlungerReading &r)
     {
-        // get the range reading
-        uint8_t d;
-        int err = sensor.getRange(d, 25000);
-        
-        // start a new reading
-        sensor.startRangeReading();
-        tStart = t.read_us();
-
-        // use the current timestamp
-        r.t = t.read_us();
+        // if we have a new reading ready, collect it
+        if (sensor.rangeReady())
+        {
+            // Get the range reading.  Note that we already know that the
+            // sensor has a reading ready, so it shouldn't be possible to 
+            // time out on the read.  (The sensor could have timed out on 
+            // convergence, but if it did, that's in the past already so 
+            // it's not something we have to wait for now.)
+            uint8_t d;
+            uint32_t t, dt;
+            lastErr = sensor.getRange(d, t, dt, 100);
+            
+            // if we got a reading, update the last reading
+            if (lastErr == 0)
+            {
+                // save the new reading
+                last.pos = d;
+                last.t = t;
+            
+                // collect scan time statistics
+                collectScanTimeStats(dt);
+            }
+    
+            // start a new reading
+            sensor.startRangeReading();
+        }
         
-        // The sensor measures distance from the front of the cabinet
-        // (in our standard setup).  For reporting purposes, we want
-        // the position reading to increase as the plunger is retracted,
-        // so we want to reverse the scale.
-        r.pos = nativeScale - d;
-        
-        // collect scan time statistics
-        if (err == 0)
-            collectScanTimeStats(uint32_t(r.t - tStart));
-
-        // return the status ('err' is zero on success)
-        return err == 0;
+        // return the most recent reading
+        r = last;
+        return lastErr == 0;
     }
     
 protected:
     // underlying sensor interface
     VL6180X sensor;
+    
+    // last reading and error status
+    PlungerReading last;
+    int lastErr;
 };
 
 
--- a/Plunger/edgeSensor.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/edgeSensor.h	Tue May 09 05:48:37 2017 +0000
@@ -18,7 +18,6 @@
 #define _EDGESENSOR_H_
 
 #include "plunger.h"
-#include "tsl14xxSensor.h"
 
 // Scan method - select a method listed below.  Method 2 (find the point
 // with maximum brightness slop) seems to work the best so far.
@@ -101,12 +100,15 @@
 // This is a generic base class for image-based sensors where we detect
 // the plunger position by finding the edge of the shadow it casts on
 // the detector.
-class PlungerSensorEdgePos
+//
+// Edge sensors use the image pixel span as the native position scale,
+// since a position reading is the pixel offset of the shadow edge.
+class PlungerSensorEdgePos: public PlungerSensorImage<int>
 {
 public:
-    PlungerSensorEdgePos(int npix)
+    PlungerSensorEdgePos(PlungerSensorImageInterface &sensor, int npix) 
+        : PlungerSensorImage<int>(sensor, npix, npix - 1)
     {
-        native_npix = npix;
     }
     
     // Process an image - scan for the shadow edge to determine the plunger
@@ -121,7 +123,7 @@
 
 #if SCAN_METHOD == 0
     // Scan method 0: one-way scan; original method used in v1 firmware.
-    bool process(const uint8_t *pix, int n, int &pos)
+    bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
     {        
         // Get the levels at each end
         int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -241,7 +243,7 @@
     
 #if SCAN_METHOD == 1
     // Scan method 1: meet in the middle.
-    bool process(const uint8_t *pix, int n, int &pos)
+    bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
     {        
         // Get the levels at each end
         int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -361,7 +363,7 @@
 
 #if SCAN_METHOD == 2
     // Scan method 2: scan for steepest brightness slope.
-    bool process(const uint8_t *pix, int n, int &pos)
+    virtual bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
     {        
         // Get the levels at each end by averaging across several pixels.
         // Compute just the sums: don't bother dividing by the count, since 
@@ -419,7 +421,7 @@
 
 #if SCAN_METHOD == 3
     // Scan method 0: one-way scan; original method used in v1 firmware.
-    bool process(const uint8_t *pix, int n, int &pos)
+    bool process(const uint8_t *pix, int n, int &pos, int& /*processResult*/)
     {        
         // Get the levels at each end
         int a = (int(pix[0]) + pix[1] + pix[2] + pix[3] + pix[4])/5;
@@ -497,9 +499,6 @@
     virtual int getOrientation() const { return dir; }
     int dir;
        
-    // number of pixels
-    int native_npix;
-    
     // History of midpoint brightness levels for the last few successful
     // scans.  This is a circular buffer that we write on each scan where
     // we successfully detect a shadow edge.  (It's circular, so we
@@ -531,62 +530,4 @@
 };
 
 
-// -------------------------------------------------------------------------
-//
-// Edge position plunger sensor for TSL14xx-based sensors.  An edge
-// detection setup requires one of the large sensors, 1410R or 1412S,
-// since we need the sensor to cover the whole extent of the physical 
-// plunger's travel, which is about 3".
-//
-// The native scale for image edge detectors is sensor pixels, since
-// we read the plunger position as the pixel location of the shadow
-// edge on the image.
-//
-class PlungerSensorEdgePosTSL14xx: public PlungerSensorTSL14xxLarge, public PlungerSensorEdgePos
-{
-public:
-    PlungerSensorEdgePosTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
-        : PlungerSensorTSL14xxLarge(nativePix, nativePix - 1, si, clock, ao),
-          PlungerSensorEdgePos(nativePix)
-    {
-        // we don't know the direction yet
-        dir = 0;
-        
-        // set the midpoint history arbitrarily to the absolute halfway point
-        memset(midpt, 127, sizeof(midpt));
-        midptIdx = 0;
-        
-        // the native reporting scale is the pixel size of the sensor, since
-        // the position is figured as the shadow location in the image
-        nativeScale = nativePix;
-    }
-    
-protected:
-    // process the image through the edge detector
-    virtual bool process(const uint8_t *pix, int npix, int &pixpos) 
-    {
-        return PlungerSensorEdgePos::process(pix, npix, pixpos);
-    }
-};
-
-// TSL1410R sensor 
-class PlungerSensorTSL1410R: public PlungerSensorEdgePosTSL14xx
-{
-public:
-    PlungerSensorTSL1410R(PinName si, PinName clock, PinName ao)
-        : PlungerSensorEdgePosTSL14xx(1280, si, clock, ao)
-    {
-    }
-};
-
-// TSL1412R
-class PlungerSensorTSL1412R: public PlungerSensorEdgePosTSL14xx
-{
-public:
-    PlungerSensorTSL1412R(PinName si, PinName clock, PinName ao)
-        : PlungerSensorEdgePosTSL14xx(1536, si, clock, ao)
-    {
-    }
-};
-
 #endif /* _EDGESENSOR_H_ */
--- a/Plunger/plunger.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/plunger.h	Tue May 09 05:48:37 2017 +0000
@@ -13,6 +13,8 @@
 #ifndef PLUNGER_H
 #define PLUNGER_H
 
+#include "config.h"
+
 // Plunger reading with timestamp
 struct PlungerReading
 {
@@ -73,6 +75,9 @@
     // working on the current reading's data transfer.
     virtual bool ready() { return true; }
     
+    // Is a plunger DMA operation in progress?
+    virtual bool dmaBusy() { return false; }
+    
     // Read the sensor position, if possible.  Returns true on success,
     // false if it wasn't possible to take a reading.  On success, fills
     // in 'r' with the current reading and timestamp and returns true.
@@ -103,7 +108,7 @@
         if (readRaw(r))
         {
             // process it through the jitter filter
-            //$$$ r.pos = jitterFilter(r.pos);
+            r.pos = jitterFilter(r.pos);
             
             // adjust to the abstract scale via the scaling factor
             r.pos = uint16_t(uint32_t((scalingFactor * r.pos) + 32768) >> 16);
@@ -195,7 +200,9 @@
             // of the window
             jfLo = pos;
             jfHi = pos + jfWindow;
-            jfLast = pos;
+            
+            // figure the new position as the centerpoint of the new window
+            jfLast = pos = (jfHi + jfLo)/2;
             return pos;
         }
         else if (pos > jfHi)
@@ -205,7 +212,9 @@
             // the window
             jfHi = pos;
             jfLo = pos - jfWindow;
-            jfLast = pos;
+
+            // figure the new position as the centerpoint of the new window
+            jfLast = pos = (jfHi + jfLo)/2;
             return pos;
         }
         else
@@ -216,6 +225,20 @@
         }
     }
     
+    // Process a configuration variable change.  'varno' is the
+    // USB protocol variable number being updated; 'cfg' is the
+    // updated configuration.
+    virtual void onConfigChange(int varno, Config &cfg)
+    {
+        switch (varno)
+        {
+        case 19:
+            // jitter window
+            setJitterWindow(cfg.plunger.jitterWindow);
+            break;
+        }
+    }
+    
     // Set the jitter filter window size.  This is specified in native
     // sensor units.
     void setJitterWindow(int w)
@@ -264,4 +287,217 @@
     int jfLast;                  // last filtered reading
 };
 
+
+// --------------------------------------------------------------------------
+//
+// Generic image sensor interface for image-based plungers
+//
+class PlungerSensorImageInterface
+{
+public:
+    PlungerSensorImageInterface(int npix)
+    {
+        native_npix = npix;
+    }
+    
+    // initialize the sensor
+    virtual void init() = 0;
+
+    // is the sensor ready?
+    virtual bool ready() = 0;
+    
+    // is a DMA transfer in progress?
+    virtual bool dmaBusy() = 0;
+
+    // read the image
+    virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime) = 0;
+    
+    // Get an image for a pixel status report.  't' is the timestamp of
+    // the image.  'extraTime' is extra exposure time for the image, in
+    // 0.1ms increments.
+    virtual void getStatusReportPixels(
+        uint8_t* &pix, uint32_t &t, int axcTime, int extraTime) = 0;
+    
+    // Reset the sensor after a status report.  Status reports take a long
+    // time to send, so sensors that use continuous integration cycling may
+    // need to reset after a status report so that they aren't overexposed
+    // by the long delay of sending the status report.
+    virtual void resetAfterStatusReport(int axcTime) = 0;
+    
+    // get the average sensor pixel scan time (the time it takes on average
+    // to read one image frame from the sensor)
+    virtual uint32_t getAvgScanTime() = 0;
+    
+protected:
+    // number of pixels on sensor
+    int native_npix;
+};
+
+
+// ----------------------------------------------------------------------------
+//
+// Plunger base class for image-based sensors
+//
+template<class ProcessResult>
+class PlungerSensorImage: public PlungerSensor
+{
+public:
+    PlungerSensorImage(PlungerSensorImageInterface &sensor, int npix, int nativeScale)
+        : PlungerSensor(nativeScale), sensor(sensor)
+    {
+        axcTime = 0;
+        native_npix = npix;
+    }
+    
+    // initialize the sensor
+    virtual void init() { sensor.init(); }
+
+    // is the sensor ready?
+    virtual bool ready() { return sensor.ready(); }
+    
+    // is a DMA transfer in progress?
+    virtual bool dmaBusy() { return sensor.dmaBusy(); }
+    
+    // get the pixel transfer time
+    virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
+
+    // read the plunger position
+    virtual bool readRaw(PlungerReading &r)
+    {
+        // read pixels from the sensor
+        uint8_t *pix;
+        uint32_t tpix;
+        sensor.readPix(pix, tpix, axcTime);
+        
+        // process the pixels
+        int pixpos;
+        ProcessResult res;
+        if (process(pix, native_npix, pixpos, res))
+        {            
+            r.pos = pixpos;
+            r.t = tpix;
+            
+            // success
+            return true;
+        }
+        else
+        {
+            // no position found
+            return false;
+        }
+    }
+
+    // Send a status report to the joystick interface.
+    // See plunger.h for details on the arguments.
+    virtual void sendStatusReport(USBJoystick &js, uint8_t flags, uint8_t extraTime)
+    {
+        // get pixels
+        uint8_t *pix;
+        uint32_t t;
+        sensor.getStatusReportPixels(pix, t, axcTime, extraTime);
+
+        // start a timer to measure the processing time
+        Timer pt;
+        pt.start();
+
+        // process the pixels and read the position
+        int pos, rawPos;
+        int n = native_npix;
+        ProcessResult res;
+        if (process(pix, n, rawPos, res))
+        {
+            // success - apply the jitter filter
+            pos = jitterFilter(rawPos);
+        }
+        else
+        {
+            // report 0xFFFF to indicate that the position wasn't read
+            pos = 0xFFFF;
+            rawPos = 0xFFFF;
+        }
+        
+        // note the processing time
+        uint32_t processTime = pt.read_us();
+        
+        // If a low-res scan is desired, reduce to a subset of pixels.  Ignore
+        // this for smaller sensors (below 512 pixels)
+        if ((flags & 0x01) && n >= 512)
+        {
+            // figure how many sensor pixels we combine into each low-res pixel
+            const int group = 8;
+            int lowResPix = n / group;
+            
+            // combine the pixels
+            int src, dst;
+            for (src = dst = 0 ; dst < lowResPix ; ++dst)
+            {
+                // average this block of pixels
+                int a = 0;
+                for (int j = 0 ; j < group ; ++j)
+                    a += pix[src++];
+                        
+                // we have the sum, so get the average
+                a /= group;
+
+                // store the down-res'd pixel in the array
+                pix[dst] = uint8_t(a);
+            }
+            
+            // update the pixel count to the reduced array size
+            n = lowResPix;
+        }
+        
+        // figure the report flags
+        int jsflags = 0;
+        
+        // add flags for the detected orientation: 0x01 for normal orientation,
+        // 0x02 for reversed orientation; no flags if orientation is unknown
+        int dir = getOrientation();
+        if (dir == 1) 
+            jsflags |= 0x01; 
+        else if (dir == -1)
+            jsflags |= 0x02;
+            
+        // send the sensor status report headers
+        js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime);
+        js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime);
+
+        // send any extra status headers for subclasses
+        extraStatusHeaders(js, res);
+        
+        // If we're not in calibration mode, send the pixels
+        extern bool plungerCalMode;
+        if (!plungerCalMode)
+        {
+            // send the pixels in report-sized chunks until we get them all
+            int idx = 0;
+            while (idx < n)
+                js.sendPlungerPix(idx, n, pix);
+        }
+        
+        // reset the sensor, if necessary
+        sensor.resetAfterStatusReport(axcTime);
+    }
+    
+protected:
+    // process an image to read the plunger position
+    virtual bool process(const uint8_t *pix, int npix, int &rawPos, ProcessResult &res) = 0;    
+    
+    // send extra status headers, following the standard headers (types 0 and 1)
+    virtual void extraStatusHeaders(USBJoystick &js, ProcessResult &res) { }
+    
+    // get the detected orientation
+    virtual int getOrientation() const { return 0; }
+    
+    // underlying hardware sensor interface
+    PlungerSensorImageInterface &sensor;
+
+    // number of pixels
+    int native_npix;
+    
+    // auto-exposure time
+    uint32_t axcTime;
+};
+
+
 #endif /* PLUNGER_H */
--- a/Plunger/tsl14xxSensor.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/Plunger/tsl14xxSensor.h	Tue May 09 05:48:37 2017 +0000
@@ -15,35 +15,35 @@
 #define _TSL14XXSENSOR_H_
 
 #include "plunger.h"
+#include "edgeSensor.h"
+#include "barCodeSensor.h"
 #include "TSL14xx.h"
 
-class PlungerSensorTSL14xx: public PlungerSensor
+class PlungerSensorImageInterfaceTSL14xx: public PlungerSensorImageInterface
 {
 public:
-    PlungerSensorTSL14xx(int nativePix, int nativeScale, 
-        PinName si, PinName clock, PinName ao)
-        : PlungerSensor(nativeScale),
-          sensor(nativePix, si, clock, ao)
+    PlungerSensorImageInterfaceTSL14xx(int nativePix, PinName si, PinName clock, PinName ao)
+        : PlungerSensorImageInterface(nativePix), sensor(nativePix, si, clock, ao)
     {
-        // remember the native pixel size
-        native_npix = nativePix;
-        
-        // start with no additional integration time for automatic 
-        // exposure control
-        axcTime = 0;
     }
         
     // is the sensor ready?
     virtual bool ready() { return sensor.ready(); }
     
+    // is a DMA transfer in progress?
+    virtual bool dmaBusy() { return sensor.dmaBusy(); }
+    
     virtual void init()
     {
         sensor.clear();
     }
     
-    // Send a status report to the joystick interface.
-    // See plunger.h for details on the arguments.
-    virtual void sendStatusReport(USBJoystick &js, uint8_t flags, uint8_t extraTime)
+    // get the average sensor scan time
+    virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
+    
+protected:
+    virtual void getStatusReportPixels(
+        uint8_t* &pix, uint32_t &t, int axcTime, int extraTime)
     {
         // The sensor's internal buffering scheme makes it a little tricky
         // to get the requested timing, and our own double-buffering adds a
@@ -90,85 +90,12 @@
         
         // wait for the DMA transfer of period B to finish, and get the 
         // period B pixels
-        uint8_t *pix;
-        uint32_t t;
         sensor.waitPix(pix, t);
-
-        // start a timer to measure the processing time
-        Timer pt;
-        pt.start();
-
-        // process the pixels and read the position
-        int pos, rawPos;
-        int n = native_npix;
-        if (process(pix, n, rawPos))
-        {
-            // success - apply the jitter filter
-            pos = jitterFilter(rawPos);
-        }
-        else
-        {
-            // report 0xFFFF to indicate that the position wasn't read
-            pos = 0xFFFF;
-            rawPos = 0xFFFF;
-        }
-        
-        // note the processing time
-        uint32_t processTime = pt.read_us();
-        
-        // If a low-res scan is desired, reduce to a subset of pixels.  Ignore
-        // this for smaller sensors (below 512 pixels)
-        if ((flags & 0x01) && n >= 512)
-        {
-            // figure how many sensor pixels we combine into each low-res pixel
-            const int group = 8;
-            int lowResPix = n / group;
-            
-            // combine the pixels
-            int src, dst;
-            for (src = dst = 0 ; dst < lowResPix ; ++dst)
-            {
-                // average this block of pixels
-                int a = 0;
-                for (int j = 0 ; j < group ; ++j)
-                    a += pix[src++];
-                        
-                // we have the sum, so get the average
-                a /= group;
-
-                // store the down-res'd pixel in the array
-                pix[dst] = uint8_t(a);
-            }
-            
-            // update the pixel count to the reduced array size
-            n = lowResPix;
-        }
-        
-        // figure the report flags
-        int jsflags = 0;
-        
-        // add flags for the detected orientation: 0x01 for normal orientation,
-        // 0x02 for reversed orientation; no flags if orientation is unknown
-        int dir = getOrientation();
-        if (dir == 1) 
-            jsflags |= 0x01; 
-        else if (dir == -1)
-            jsflags |= 0x02;
-            
-        // send the sensor status report headers
-        js.sendPlungerStatus(n, pos, jsflags, sensor.getAvgScanTime(), processTime);
-        js.sendPlungerStatus2(nativeScale, jfLo, jfHi, rawPos, axcTime);
-        
-        // If we're not in calibration mode, send the pixels
-        extern bool plungerCalMode;
-        if (!plungerCalMode)
-        {
-            // send the pixels in report-sized chunks until we get them all
-            int idx = 0;
-            while (idx < n)
-                js.sendPlungerPix(idx, n, pix);
-        }
-            
+    }
+    
+    // reset after a status report
+    virtual void resetAfterStatusReport(int axcTime)
+    {
         // It takes us a while to send all of the pixels, since we have
         // to break them up into many USB reports.  This delay means that
         // the sensor has been sitting there integrating for much longer
@@ -179,39 +106,9 @@
         sensor.clear();
         sensor.startCapture(axcTime);
     }
-    
-    // get the average sensor scan time
-    virtual uint32_t getAvgScanTime() { return sensor.getAvgScanTime(); }
-    
-protected:
-    // Analyze the image and find the plunger position.  If successful,
-    // fills in 'pixpos' with the plunger position using the 0..65535
-    // scale and returns true.  If no position can be detected from the
-    // image data, returns false.
-    virtual bool process(const uint8_t *pix, int npix, int &pixpos) = 0;
-    
-    // Get the currently detected sensor orientation, if applicable.
-    // Returns 1 for standard orientation, -1 for reversed orientation,
-    // or 0 for orientation unknown or not applicable.  Edge sensors can
-    // automatically detect orientation by observing which side of the
-    // image is in shadow.  Bar code sensors generally can't detect
-    // orientation.
-    virtual int getOrientation() const { return 0; }
-    
+
     // the low-level interface to the TSL14xx sensor
     TSL14xx sensor;
-    
-    // number of pixels
-    int native_npix;
-
-    // Automatic exposure control time, in microseconds.  This is an amount
-    // of time we add to each integration cycle to compensate for low light
-    // levels.  By default, this is always zero; the base class doesn't have
-    // any logic for determining proper exposure, because that's a function
-    // of the type of image we're looking for.  Subclasses can add logic in
-    // the process() function to check exposure level and adjust this value
-    // if the image looks over- or under-exposed.
-    uint32_t axcTime;
 };
 
 // ---------------------------------------------------------------------
@@ -245,42 +142,23 @@
 // but that would complicate things considerably since our image
 // analysis is too time-consuming to do in interrupt context.
 //
-class PlungerSensorTSL14xxLarge: public PlungerSensorTSL14xx
+class PlungerSensorTSL14xxLarge: public PlungerSensorImageInterfaceTSL14xx
 {
 public:
-    PlungerSensorTSL14xxLarge(int nativePix, int nativeScale, 
-        PinName si, PinName clock, PinName ao)
-        : PlungerSensorTSL14xx(nativePix, nativeScale, si, clock, ao)
+    PlungerSensorTSL14xxLarge(int nativePix, PinName si, PinName clock, PinName ao)
+        : PlungerSensorImageInterfaceTSL14xx(nativePix, si, clock, ao)
     {
     }
 
-    // read the plunger position
-    virtual bool readRaw(PlungerReading &r)
-    {
+    virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime)
+    {        
         // start reading the next pixel array (this waits for any DMA
         // transfer in progress to finish, ensuring a stable pixel buffer)
         sensor.startCapture(axcTime);
 
         // get the image array from the last capture
-        uint8_t *pix;
-        uint32_t tpix;
-        sensor.getPix(pix, tpix);
+        sensor.getPix(pix, t);
         
-        // process the pixels
-        int pixpos;
-        if (process(pix, native_npix, pixpos))
-        {            
-            r.pos = pixpos;
-            r.t = tpix;
-            
-            // success
-            return true;
-        }
-        else
-        {
-            // no position found
-            return false;
-        }
     }
 };        
 
@@ -307,17 +185,16 @@
 // transferring period A's pixels into a DMA buffer.  We want
 // those period A pixels, so we wait for this transfer to finish.
 //
-class PlungerSensorTSL14xxSmall: public PlungerSensorTSL14xx
+class PlungerSensorTSL14xxSmall: public PlungerSensorImageInterfaceTSL14xx
 {
 public:
-    PlungerSensorTSL14xxSmall(int nativePix, int nativeScale, 
-        PinName si, PinName clock, PinName ao)
-        : PlungerSensorTSL14xx(nativePix, nativeScale, si, clock, ao)
+    PlungerSensorTSL14xxSmall(int nativePix, PinName si, PinName clock, PinName ao)
+        : PlungerSensorImageInterfaceTSL14xx(nativePix, si, clock, ao)
     {
     }
 
-    // read the plunger position
-    virtual bool readRaw(PlungerReading &r)
+    // read the image
+    virtual void readPix(uint8_t* &pix, uint32_t &t, int axcTime)
     {
         // Clear the sensor.  This sends a HOLD/SI pulse to the sensor,
         // which ends the current integration period, starts a new one
@@ -339,27 +216,55 @@
         
         // wait for the period A pixel transfer to finish, and grab
         // its pixels
-        uint8_t *pix;
-        uint32_t tpix;
-        sensor.waitPix(pix, tpix);
-        
-        // process the pixels
-        int pixpos;
-        if (process(pix, native_npix, pixpos))
-        {            
-            r.pos = pixpos;
-            r.t = tpix;
-            
-            // success
-            return true;
-        }
-        else
-        {
-            // no position found
-            return false;
-        }
+        sensor.waitPix(pix, t);
     }
 };        
 
 
+// -------------------------------------------------------------------------
+//
+// Concrete TSL14xx sensor types
+//
+
+
+// TSL1410R sensor - edge detection sensor
+class PlungerSensorTSL1410R: public PlungerSensorEdgePos
+{
+public:
+    PlungerSensorTSL1410R(PinName si, PinName clock, PinName ao)
+        : PlungerSensorEdgePos(sensor, 1280), sensor(1280, si, clock, ao)
+    {
+    }
+    
+protected:
+    PlungerSensorTSL14xxLarge sensor;
+};
+
+// TSL1412R - edge detection sensor
+class PlungerSensorTSL1412R: public PlungerSensorEdgePos
+{
+public:
+    PlungerSensorTSL1412R(PinName si, PinName clock, PinName ao)
+        : PlungerSensorEdgePos(sensor, 1536), sensor(1536, si, clock, ao)
+    {
+    }
+    
+protected:
+    PlungerSensorTSL14xxLarge sensor;
+};
+
+// TSL1401CL - bar code sensor
+class PlungerSensorTSL1401CL: public PlungerSensorBarCode<7, 0, 1, 16>
+{
+public:
+    PlungerSensorTSL1401CL(PinName si, PinName clock, PinName ao)
+        : PlungerSensorBarCode(sensor, 128), sensor(128, si, clock, ao)
+    {
+    }
+    
+protected:
+    PlungerSensorTSL14xxSmall sensor;
+};
+
+
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TLC59116/TLC59116.h	Tue May 09 05:48:37 2017 +0000
@@ -0,0 +1,473 @@
+// TLC59116 interface
+//
+// The TLC59116 is a 16-channel constant-current PWM controller chip with
+// an I2C interface.
+//
+// Up to 14 of these chips can be connected to a single bus.  Each chip needs
+// a unique address, configured via four pin inputs.  (The I2C address is 7
+// bits, but the high-order 3 bits are fixed in the hardware, leaving 4 bits
+// to configure per chip.  Two of the possible 16 addresses are reserved by
+// the chip hardware as broadcast addresses, leaving room for 14 unique chip
+// addresses per bus.)
+//
+// EXTERNAL PULL-UP RESISTORS ARE REQUIRED ON SDA AND SCL.  The internal 
+// pull-ups in the KL25Z GPIO ports will only work if the bus speed is 
+// limited to 100kHz.  Higher speeds require external pull-ups.  Because
+// of the relatively high data rate required, we use the maximum 1MHz bus 
+// speed, requiring external pull-ups.  These are typically 2.2K.
+//
+// This chip is similar to the TLC5940, but has a more modern design with 
+// several advantages, including a standardized and much more robust data 
+// interface (I2C) and glitch-free startup.  The only downside vs the TLC5940 
+// is that it's only available in an SMD package, whereas the TLC5940 is 
+// available in easy-to-solder DIP format.  The DIP 5940 is longer being 
+// manufactured, but it's still easy to find old stock; when those run out,
+// though, and the choice is between SMD 5940 and 59116, the 59116 will be
+// the clear winner.
+//
+
+#ifndef _TLC59116_H_
+#define _TLC59116_H_
+
+#include "mbed.h"
+#include "BitBangI2C.h"
+
+// Which I2C class are we using?  We use this to switch between
+// BitBangI2C and MbedI2C for testing and debugging.
+#define I2C_Type BitBangI2C
+
+// register constants
+struct TLC59116R
+{
+    // control register bits
+    static const uint8_t CTL_AIALL = 0x80;         // auto-increment mode, all registers
+    static const uint8_t CTL_AIPWM = 0xA0;         // auto-increment mode, PWM registers only
+    static const uint8_t CTL_AICTL = 0xC0;         // auto-increment mode, control registers only
+    static const uint8_t CTL_AIPWMCTL = 0xE0;      // auto-increment mode, PWM + control registers only
+
+    // register addresses
+    static const uint8_t REG_MODE1 = 0x00;         // MODE1
+    static const uint8_t REG_MODE2 = 0x01;         // MODE2
+    static const uint8_t REG_PWM0 = 0x02;          // PWM 0
+    static const uint8_t REG_PWM1 = 0x03;          // PWM 1
+    static const uint8_t REG_PWM2 = 0x04;          // PWM 2
+    static const uint8_t REG_PWM3 = 0x05;          // PWM 3
+    static const uint8_t REG_PWM4 = 0x06;          // PWM 4
+    static const uint8_t REG_PWM5 = 0x07;          // PWM 5
+    static const uint8_t REG_PWM6 = 0x08;          // PWM 6
+    static const uint8_t REG_PWM7 = 0x09;          // PWM 7
+    static const uint8_t REG_PWM8 = 0x0A;          // PWM 8
+    static const uint8_t REG_PWM9 = 0x0B;          // PWM 9
+    static const uint8_t REG_PWM10 = 0x0C;         // PWM 10
+    static const uint8_t REG_PWM11 = 0x0D;         // PWM 11
+    static const uint8_t REG_PWM12 = 0x0E;         // PWM 12
+    static const uint8_t REG_PWM13 = 0x0F;         // PWM 13
+    static const uint8_t REG_PWM14 = 0x10;         // PWM 14
+    static const uint8_t REG_PWM15 = 0x11;         // PWM 15
+    static const uint8_t REG_GRPPWM = 0x12;        // Group PWM duty cycle
+    static const uint8_t REG_GRPFREQ = 0x13;       // Group frequency register
+    static const uint8_t REG_LEDOUT0 = 0x14;       // LED driver output status register 0
+    static const uint8_t REG_LEDOUT1 = 0x15;       // LED driver output status register 1
+    static const uint8_t REG_LEDOUT2 = 0x16;       // LED driver output status register 2
+    static const uint8_t REG_LEDOUT3 = 0x17;       // LED driver output status register 3
+    
+    // MODE1 bits
+    static const uint8_t MODE1_AI2 = 0x80;         // auto-increment mode enable
+    static const uint8_t MODE1_AI1 = 0x40;         // auto-increment bit 1
+    static const uint8_t MODE1_AI0 = 0x20;         // auto-increment bit 0
+    static const uint8_t MODE1_OSCOFF = 0x10;      // oscillator off
+    static const uint8_t MODE1_SUB1 = 0x08;        // subaddress 1 enable
+    static const uint8_t MODE1_SUB2 = 0x04;        // subaddress 2 enable
+    static const uint8_t MODE1_SUB3 = 0x02;        // subaddress 3 enable
+    static const uint8_t MODE1_ALLCALL = 0x01;     // all-call enable
+    
+    // MODE2 bits
+    static const uint8_t MODE2_EFCLR = 0x80;       // clear error status flag
+    static const uint8_t MODE2_DMBLNK = 0x20;      // group blinking mode
+    static const uint8_t MODE2_OCH = 0x08;         // outputs change on ACK (vs Stop command)
+    
+    // LEDOUTn states
+    static const uint8_t LEDOUT_OFF = 0x00;        // driver is off
+    static const uint8_t LEDOUT_ON = 0x01;         // fully on
+    static const uint8_t LEDOUT_PWM = 0x02;        // individual PWM control via PWMn register
+    static const uint8_t LEDOUT_GROUP = 0x03;      // PWM control + group dimming/blinking via PWMn + GRPPWM
+};
+   
+
+// Individual unit object.  We create one of these for each unit we
+// find on the bus.  This keeps track of the state of each output on
+// a unit so that we can update outputs in batches, to reduce the 
+// amount of time we spend in I2C communications during rapid updates.
+struct TLC59116Unit
+{
+    TLC59116Unit()
+    {
+        // start inactive, since we haven't been initialized yet
+        active = false;
+        
+        // set all brightness levels to 0 intially
+        memset(bri, 0, sizeof(bri));
+        
+        // mark all outputs as dirty to force an update after initializing
+        dirty = 0xFFFF;
+    }
+    
+    // initialize
+    void init(int addr, I2C_Type &i2c)
+    {        
+        // set all output drivers to individual PWM control
+        const uint8_t all_pwm = 
+            TLC59116R::LEDOUT_PWM 
+            | (TLC59116R::LEDOUT_PWM << 2)
+            | (TLC59116R::LEDOUT_PWM << 4)
+            | (TLC59116R::LEDOUT_PWM << 6);
+        static const uint8_t buf[] = { 
+            TLC59116R::REG_LEDOUT0 | TLC59116R::CTL_AIALL,
+            all_pwm, 
+            all_pwm, 
+            all_pwm, 
+            all_pwm 
+        };
+        int err = i2c.write(addr << 1, buf, sizeof(buf));
+
+        // turn on the oscillator
+        static const uint8_t buf2[] = { 
+            TLC59116R::REG_MODE1, 
+            TLC59116R::MODE1_AI2 | TLC59116R::MODE1_ALLCALL 
+        };
+        err |= i2c.write(addr << 1, buf2, sizeof(buf));
+        
+        // mark the unit as active if the writes succeeded
+        active = !err;
+    }
+    
+    // Set an output
+    void set(int idx, int val)
+    {
+        // validate the index
+        if (idx >= 0 && idx <= 15)
+        {
+            // record the new brightness
+            bri[idx] = val;
+            
+            // set the dirty bit
+            dirty |= 1 << idx;
+        }
+    }
+    
+    // Get an output's current value
+    int get(int idx) const
+    {
+        return idx >= 0 && idx <= 15 ? bri[idx] : -1;
+    }
+    
+    // Send I2C updates
+    void send(int addr, I2C_Type &i2c)
+    {
+        // Scan all outputs.  I2C sends are fairly expensive, so we
+        // minimize the send time by using the auto-increment mode.
+        // Optimizing this is a bit tricky.  Suppose that the outputs
+        // are in this state, where c represents a clean output and D
+        // represents a dirty output:
+        //
+        //    cccDcDccc...
+        //
+        // Clearly we want to start sending at the first dirty output
+        // so that we don't waste time sending the three clean bytes
+        // ahead of it.  However, do we send output[3] as one chunk
+        // and then send output[5] as a separate chunk, or do we send
+        // outputs [3],[4],[5] as a single block to take advantage of
+        // the auto-increment mode?  Based on I2C bus timing parameters,
+        // the answer is that it's cheaper to send this as a single
+        // contiguous block [3],[4],[5].  The reason is that the cost
+        // of starting a new block is a Stop/Start sequence plus another
+        // register address byte; the register address byte costs the
+        // same as a data byte, so the extra Stop/Start of the separate
+        // chunk approach makes the single continguous send cheaper. 
+        // But how about this one?:
+        //
+        //   cccDccDccc...
+        //
+        // This one is cheaper to send as two separate blocks.  The
+        // break costs us a Start/Stop plus a register address byte,
+        // but the Start/Stop is only about 25% of the cost of a data
+        // byte, so Start/Stop+Register Address is cheaper than sending
+        // the two clean data bytes sandwiched between the dirty bytes.
+        //
+        // So: we want to look for sequences of contiguous dirty bytes
+        // and send those as a chunk.  We furthermore will allow up to
+        // one clean byte in the midst of the dirty bytes.
+        uint8_t buf[17];
+        int n = 0;
+        for (int i = 0, bit = 1 ; i < 16 ; ++i, bit <<= 1)
+        {
+            // If this one is dirty, include it in the set of outputs to
+            // send to the chip.  Also include this one if it's clean
+            // and the outputs on both sides are dirty - see the notes
+            // above about optimizing for the case where we have one clean
+            // output surrounded by dirty outputs.
+            if ((dirty & bit) != 0)
+            {
+                // it's dirty - add it to the dirty set under construction
+                buf[++n] = bri[i];
+            }
+            else if (n != 0 && n < 15 && (dirty & (bit << 1)) != 0)
+            {
+                // this one is clean, but the one before and the one after
+                // are both dirty, so keep it in the set anyway to take
+                // advantage of the auto-increment mode for faster sends
+                buf[++n] = bri[i];
+            }
+            else
+            {
+                // This one is clean, and it's not surrounded by dirty
+                // outputs.  If the set of dirty outputs so far has any
+                // members, send them now.
+                if (n != 0)
+                {
+                    // set the starting register address, including the
+                    // auto-increment flag, and write the block
+                    buf[0] = (TLC59116R::REG_PWM0 + i - n) | TLC59116R::CTL_AIALL;
+                    i2c.write(addr << 1, buf, n + 1);
+                    
+                    // empty the set
+                    n = 0;
+                }
+            }
+        }
+        
+        // if we finished the loop with dirty outputs to send, send them
+        if (n != 0)
+        {
+            // fill in the starting register address, and write the block
+            buf[0] = (TLC59116R::REG_PWM15 + 1 - n) | TLC59116R::CTL_AIALL;
+            i2c.write(addr << 1, buf, n + 1);
+        }
+        
+        // all outputs are now clean
+        dirty = 0;
+    }
+    
+    // Is the unit active?  If we have trouble writing a unit,
+    // we can mark it inactive so that we know to stop wasting
+    // time writing to it, and so that we can re-initialize it
+    // if it comes back on later bus scans.
+    bool active;
+    
+    // Output states.  This records the latest brightness level
+    // for each output as set by the client.  We don't actually
+    // send these values to the physical unit until the client 
+    // tells us to do an I2C update.
+    uint8_t bri[16];
+    
+    // Dirty output mask.  Whenever the client changes an output,
+    // we record the new brightness in bri[] and set the 
+    // corresponding bit here to 1.  We use these bits to determine
+    // which outputs to send during each I2C update.
+    uint16_t dirty;
+};
+
+// TLC59116 public interface.  This provides control over a collection
+// of units connected on a common I2C bus.
+class TLC59116
+{
+public:
+    // Initialize.  The address given is the configurable part
+    // of the address, 0x0000 to 0x000F.
+    TLC59116(PinName sda, PinName scl, PinName reset)
+        : i2c(sda, scl, true), reset(reset)
+    {
+        // Use the fastest I2C speed possible, since we want to be able
+        // to rapidly update many outputs at once.  The TLC59116 can run 
+        // I2C at up to 1MHz.
+        i2c.frequency(1000000);
+        
+        // assert !RESET until we're ready to go
+        this->reset.write(0);
+        
+        // there are no units yet
+        memset(units, 0, sizeof(units));
+        nextUpdate = 0;
+    }
+    
+    void init()
+    {
+        // un-assert reset
+        reset.write(1);
+        wait_us(10000);
+        
+        // scan the bus for new units
+        scanBus();
+    }
+    
+    // scan the bus
+    void scanBus()
+    {
+        // scan each possible address
+        for (int i = 0 ; i < 16 ; ++i)
+        {
+            // Address 8 and 11 are reserved - skip them
+            if (i == 8 || i == 11)
+                continue;
+                
+            // Try reading register REG_MODE1
+            int addr = I2C_BASE_ADDR | i;
+            TLC59116Unit *u = units[i];
+            if (readReg8(addr, TLC59116R::REG_MODE1) >= 0)
+            {
+                // success - if the slot wasn't already populated, allocate
+                // a unit entry for it
+                if (u == 0)
+                    units[i] = u = new TLC59116Unit();
+                    
+                // if the unit isn't already marked active, initialize it
+                if (!u->active)
+                    u->init(addr, i2c);
+            }
+            else
+            {
+                // failed - if the unit was previously active, mark it
+                // as inactive now
+                if (u != 0)
+                    u->active = false;
+            }
+        }
+    }
+    
+    // set an output
+    void set(int unit, int output, int val)
+    {
+        if (unit >= 0 && unit <= 15)
+        {
+            TLC59116Unit *u = units[unit];
+            if (u != 0)
+                u->set(output, val);
+        }
+    }
+    
+    // get an output's current value
+    int get(int unit, int output)
+    {
+        if (unit >= 0 && unit <= 15)
+        {
+            TLC59116Unit *u = units[unit];
+            if (u != 0)
+                return u->get(output);
+        }
+        
+        return -1;
+    }
+    
+    // Send I2C updates to the next unit.  The client must call this 
+    // periodically to send pending updates.  We only update one unit on 
+    // each call to ensure that the time per cycle is relatively constant
+    // (rather than scaling with the number of chips).
+    void send()
+    {
+        // look for a dirty unit
+        for (int i = 0, n = nextUpdate ; i < 16 ; ++i, ++n)
+        {
+            // wrap the unit number
+            n &= 0x0F;
+            
+            // if this unit is populated and dirty, it's the one to update
+            TLC59116Unit *u = units[n];
+            if (u != 0 && u->dirty != 0)
+            {
+                // it's dirty - update it 
+                u->send(I2C_BASE_ADDR | n, i2c);
+                
+                // We only update one on each call, so we're done.
+                // Remember where to pick up again on the next update() 
+                // call, and return.
+                nextUpdate = n + 1;
+                return;
+            }
+        }
+    }
+    
+    // Enable/disable all outputs
+    void enable(bool f)
+    {
+        // visit each populated unit
+        for (int i = 0 ; i < 16 ; ++i)
+        {
+            // if this unit is populated, enable/disable it
+            TLC59116Unit *u = units[i];
+            if (u != 0)
+            {
+                // read the current MODE1 register
+                int m = readReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1);
+                if (m >= 0)
+                {
+                    // Turn the oscillator off to disable, on to enable. 
+                    // Note that the bit is kind of backwards:  SETTING the 
+                    // OSC bit turns the oscillator OFF.
+                    if (f)
+                        m &= ~TLC59116R::MODE1_OSCOFF; // enable - clear the OSC bit
+                    else
+                        m |= TLC59116R::MODE1_OSCOFF;  // disable - set the OSC bit
+                        
+                    // update MODE1
+                    writeReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1, m);
+                }
+            }
+        }
+    }
+    
+protected:
+    // TLC59116 base I2C address.  These chips use an address of
+    // the form 110xxxx, where the the low four bits are set by
+    // external pins on the chip.  The top three bits are always
+    // the same, so we construct the full address by combining 
+    // the upper three fixed bits with the four-bit unit number.
+    //
+    // Note that addresses 1101011 (0x6B) and 1101000 (0x68) are
+    // reserved (for SWRSTT and ALLCALL, respectively), and can't
+    // be used for configured device addresses.
+    static const uint8_t I2C_BASE_ADDR = 0x60;
+    
+    // Units.  We populate this with active units we find in
+    // bus scans.  Note that units 8 and 11 can't be used because
+    // of the reserved ALLCALL and SWRST addresses, but we allocate
+    // the slots anyway to keep indexing simple.
+    TLC59116Unit *units[16];
+    
+    // next unit to update
+    int nextUpdate;
+
+    // read 8-bit register; returns the value read on success, -1 on failure
+    int readReg8(int addr, uint16_t registerAddr)
+    {
+        // write the request - register address + auto-inc mode
+        uint8_t data_write[1];
+        data_write[0] = registerAddr | TLC59116R::CTL_AIALL;
+        if (i2c.write(addr << 1, data_write, 1, true))
+            return -1;
+    
+        // read the result
+        uint8_t data_read[1];
+        if (i2c.read(addr << 1, data_read, 1))
+            return -1;
+        
+        // return the result
+        return data_read[0];
+    }
+ 
+    // write 8-bit register; returns true on success, false on failure
+    bool writeReg8(int addr, uint16_t registerAddr, uint8_t data)
+    {
+        uint8_t data_write[2];
+        data_write[0] = registerAddr | TLC59116R::CTL_AIALL;
+        data_write[1] = data;
+        return !i2c.write(addr << 1, data_write, 2);
+    }
+ 
+    // I2C bus interface
+    I2C_Type i2c;
+    
+    // reset pin (active low)
+    DigitalOut reset;
+};
+
+#endif
--- a/TLC5940/TLC5940.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/TLC5940/TLC5940.h	Tue May 09 05:48:37 2017 +0000
@@ -392,16 +392,6 @@
         }
     }
     
-    // Update the outputs.  In our current implementation, this doesn't do
-    // anything, since we send the current state to the chips on every grayscale
-    // cycle, whether or not there are updates.  We provide the interface for
-    // consistency with other peripheral device interfaces in the main loop,
-    // and in case we make any future implementation changes that require some
-    // action to carry out an explicit update.
-    void update(bool force = false)
-    {
-    }
-    
     // Send updates if ready.  Our top-level program's main loop calls this on
     // every iteration.  This lets us send grayscale updates to the chips in
     // regular application context (rather than in interrupt context), to keep
--- a/TSL14xx/TSL14xx.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/TSL14xx/TSL14xx.h	Tue May 09 05:48:37 2017 +0000
@@ -480,6 +480,9 @@
     
     // Is the latest reading ready?
     bool ready() const { return !running; }
+    
+    // Is a DMA transfer in progress?
+    bool dmaBusy() const { return running; }
         
     // Clock through all pixels to clear the array.  Pulses SI at the
     // beginning of the operation, which starts a new integration cycle.
--- a/USBJoystick/USBJoystick.cpp	Fri Apr 21 18:50:37 2017 +0000
+++ b/USBJoystick/USBJoystick.cpp	Tue May 09 05:48:37 2017 +0000
@@ -94,7 +94,7 @@
 }
  
 bool USBJoystick::sendPlungerStatus(
-    int npix, int edgePos, int flags, uint32_t avgScanTime, uint32_t processingTime)
+    int npix, int plungerPos, int flags, uint32_t avgScanTime, uint32_t processingTime)
 {
     HID_REPORT report;
     
@@ -112,8 +112,8 @@
     put(ofs, uint16_t(npix));
     ofs += 2;
     
-    // write the shadow edge position to bytes 5-6
-    put(ofs, uint16_t(edgePos));
+    // write the detected plunger position to bytes 5-6
+    put(ofs, uint16_t(plungerPos));
     ofs += 2;
     
     // Add the calibration mode flag if applicable
@@ -191,6 +191,45 @@
     return sendTO(&report, 100);
 }
 
+bool USBJoystick::sendPlungerStatusBarcode(
+        int nbits, int codetype, int startOfs, int pixPerBit, int raw, int mask)
+{
+    HID_REPORT report;
+    memset(report.data, 0, sizeof(report.data));
+    
+    // Set the special status bits to indicate it's an extended
+    // exposure report.
+    put(0, 0x87FF);
+    
+    // start at the second byte
+    int ofs = 2;
+    
+    // write the report subtype (2) to byte 2
+    report.data[ofs++] = 2;
+
+    // write the bit count and code type
+    report.data[ofs++] = nbits;
+    report.data[ofs++] = codetype;
+    
+    // write the bar code starting pixel offset
+    put(ofs, uint16_t(startOfs));
+    ofs += 2;
+    
+    // write the pixel width per bit
+    report.data[ofs++] = pixPerBit;
+    
+    // write the raw bar code and success bit mask
+    put(ofs, uint16_t(raw));
+    ofs += 2;
+    put(ofs, uint16_t(mask));
+    ofs += 2;
+    
+    // send the report
+    report.length = reportLen;
+    return sendTO(&report, 100);
+}
+
+
 bool USBJoystick::sendPlungerPix(int &idx, int npix, const uint8_t *pix)
 {
     HID_REPORT report;
--- a/USBJoystick/USBJoystick.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/USBJoystick/USBJoystick.h	Tue May 09 05:48:37 2017 +0000
@@ -176,6 +176,18 @@
       */
     bool sendPlungerStatus2(
         int nativeScale, int jitterLo, int jitterHi, int rawPos, int axcTime);
+        
+    /**
+     * Send a barcode plunger status report header.
+     *
+     * @param nbits number of bits in bar code
+     * @param codetype bar code type (1=Gray code/Manchester bit coding)
+     * @param pixofs pixel offset of first bit
+     * @param raw raw bar code bits
+     * @param mask mask of successfully read bar code bits
+     */
+    bool sendPlungerStatusBarcode(
+        int nbits, int codetype, int startOfs, int pixPerBit, int raw, int mask);
     
     /**
      * Write an exposure report.  We'll fill out a report with as many pixels as
--- a/USBProtocol.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/USBProtocol.h	Tue May 09 05:48:37 2017 +0000
@@ -172,6 +172,21 @@
 //                status as of this reading.
 //    bytes 9:10 = Raw sensor reading before jitter filter was applied.
 //    bytes 11:12 = Auto-exposure time in microseconds
+//
+// An optional third message provides additional information specifically
+// for bar-code sensors:
+//
+//    bytes 0:1 = 0x87FF
+//    byte  2   = 2 -> bar code status report
+//    byte  3   = number of bits in bar code
+//    byte  4   = bar code type:
+//                  1 = Gray code/Manchester bit coding
+//    bytes 5:6 = pixel offset of first bit
+//    byte  7   = width in pixels of each bit
+//    bytes 8:9 = raw bar code bits
+//    bytes 10:11 = mask of successfully read bar code bits; a '1' bit means
+//                that the bit was read successfully, '0' means the bit was
+//                unreadable
 //   
 //
 // If the sensor is an imaging sensor type, this will be followed by a
@@ -182,7 +197,7 @@
 // zero, so obviously no pixel packets will follow.  If the "calibration
 // active" bit in the flags byte is set, no pixel packets are sent even
 // if the sensor is an imaging type, since the transmission time for the
-// pixels would intefere with the calibration process.  If pixels are sent,
+// pixels would interfere with the calibration process.  If pixels are sent,
 // they're sent in order starting at the first pixel.  The format of each 
 // pixel packet is:
 //
@@ -898,7 +913,7 @@
 //       finishes.  This allows TVs to be turned on via IR remotes codes rather than
 //       hard-wiring them through the relay.  The relay can be omitted in this case.
 //
-// 10 -> TLC5940NT setup.  This chip is an external PWM controller, with 32 outputs
+// 10 -> TLC5940NT setup.  This chip is an external PWM controller, with 16 outputs
 //       per chip and a serial data interface that allows the chips to be daisy-
 //       chained.  We can use these chips to add an arbitrary number of PWM output 
 //       ports for the LedWiz emulation.
@@ -1072,6 +1087,44 @@
 //
 //       byte 3:4 = window size in joystick units, little-endian
 //
+// 20 -> Plunger bar code setup.  Sets parameters applicable only to bar code
+//       sensor types.
+//
+//       bytes 3:4 = Starting pixel offset of bar code (margin width)
+//
+// 21 -> TLC59116 setup.  This chip is an external PWM controller with 16
+//       outputs per chip and an I2C bus interface.  Up to 14 of the chips
+//       can be connected to a single bus.  This chip is a successor to the 
+//       TLC5940 with a more modern design and some nice improvements, such 
+//       as glitch-free startup and a standard (I2C) physical interface.
+//
+//       Each chip has a 7-bit I2C address.  The top three bits of the
+//       address are fixed in the chip itself and can't be configured, but
+//       the low four bits are configurable via the address line pins on
+//       the chip, A3 A2 A1 A0.  Our convention here is to ignore the fixed
+//       three bits and refer to the chip address as just the A3 A2 A1 A0
+//       bits.  This gives each chip an address from 0 to 15.
+//
+//       I2C allows us to discover the attached chips automatically, so in
+//       principle we don't need to know which chips will be present.  
+//       However, it's useful for the config tool to know which chips are
+//       expected so that it can offer them in the output port setup UI.
+//       We therefore provide a bit mask specifying the enabled chips.  Each
+//       bit specifies whether the chip at the corresponding address is
+//       present: 0x0001 is the chip at address 0, 0x0002 is the chip at
+//       address 1, etc.  This is mostly for the config tool's use; we only
+//       use it to determine if TLC59116 support should be enabled at all,
+//       by checking if it's non-zero.
+//
+//       To disable support, set the populated chip mask to 0.  The pin
+//       assignments are all ignored in this case.
+//
+//          bytes 3:4 = populated chips, as a bit mask (OR in 1<<address
+//                   each populated address)
+//          byte 5 = SDA (any GPIO pin)
+//          byte 6 = SCL (any GPIO pin)
+//          byte 7 = RESET (any GPIO pin)
+//
 //
 // SPECIAL DIAGNOSTICS VARIABLES:  These work like the array variables below,
 // the only difference being that we don't report these in the number of array
@@ -1225,42 +1278,63 @@
 //        regardless of the settings for post 34 and higher.
 //
 //        The bytes of the message are:
+//
 //          byte 3 = LedWiz port number (1 to MAX_OUT_PORTS)
+//
 //          byte 4 = physical output type:
+//
 //                    0 = Disabled.  This output isn't used, and isn't visible to the
 //                        LedWiz/DOF software on the host.  The FIRST disabled port
 //                        determines the number of ports visible to the host - ALL ports
 //                        after the first disabled port are also implicitly disabled.
+//
 //                    1 = GPIO PWM output: connected to GPIO pin specified in byte 5,
 //                        operating in PWM mode.  Note that only a subset of KL25Z GPIO
 //                        ports are PWM-capable.
+//
 //                    2 = GPIO Digital output: connected to GPIO pin specified in byte 5,
 //                        operating in digital mode.  Digital ports can only be set ON
 //                        or OFF, with no brightness/intensity control.  All pins can be
 //                        used in this mode.
+//
 //                    3 = TLC5940 port: connected to TLC5940 output port number specified 
 //                        in byte 5.  Ports are numbered sequentially starting from port 0
 //                        for the first output (OUT0) on the first chip in the daisy chain.
+//
 //                    4 = 74HC595 port: connected to 74HC595 output port specified in byte 5.
 //                        As with the TLC5940 outputs, ports are numbered sequentially from 0
 //                        for the first output on the first chip in the daisy chain.
+//
 //                    5 = Virtual output: this output port exists for the purposes of the
 //                        LedWiz/DOF software on the host, but isn't physically connected
 //                        to any output device.  This can be used to create a virtual output
 //                        for the DOF ZB Launch Ball signal, for example, or simply as a
 //                        placeholder in the LedWiz port numbering.  The physical output ID 
 //                        (byte 5) is ignored for this port type.
+//
+//                    6 = TLC59116 output: connected to the TLC59116 output port specified
+//                        in byte 5.  The high four bits of this value give the chip's
+//                        I2C address, specifically the A3 A2 A1 A0 bits configured in
+//                        the hardware.  (A chip's I2C address is actually 7 bits, but
+//                        the three high-order bits are fixed, so we don't bother including
+//                        those in the byte 5 value).  The low four bits of this value
+//                        give the output port number on the chip.  For example, 0x37
+//                        specifies chip 3 (the one with A3 A2 A1 A0 wired as 0 0 1 1),
+//                        output #7 on that chip.  Note that outputs are numbered from 0
+//                        to 15 (0xF) on each chip.
+//
 //          byte 5 = physical output port, interpreted according to the value in byte 4
+//
 //          byte 6 = flags: a combination of these bit values:
 //                    0x01 = active-high output (0V on output turns attached device ON)
 //                    0x02 = noisemaker device: disable this output when "night mode" is engaged
 //                    0x04 = apply gamma correction to this output
 //
-//        Note that the on-board LED segments can be used as LedWiz output ports.  This
+//        Note that the KL25Z's on-board LEDs 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.
+//        having to connect any external devices.  Assigning the on-board LEDs as output
+//        ports overrides their normal status/diagnostic display use, so the normal status 
+//        flash pattern won't appear when they're used this way.
 //
 
 
--- a/VL6180X/VL6180X.cpp	Fri Apr 21 18:50:37 2017 +0000
+++ b/VL6180X/VL6180X.cpp	Tue May 09 05:48:37 2017 +0000
@@ -3,16 +3,19 @@
 #include "mbed.h"
 #include "VL6180X.h"
 
-VL6180X::VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0)
-    : i2c(sda, scl), gpio0Pin(gpio0)
+VL6180X::VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0, 
+    bool internalPullups)
+    : i2c(sda, scl, internalPullups), gpio0Pin(gpio0)
 {
     // remember the address
     this->addr = addr;
     
     // start in single-shot distance mode
     distMode = 0;
+    rangeStarted = false;
     
-    // initially reset the sensor
+    // initially reset the sensor by holding GPIO0/CE low
+    gpio0Pin.mode(PullNone);
     gpio0Pin.output();
     gpio0Pin.write(0);
 }
@@ -28,10 +31,10 @@
     gpio0Pin.write(0);
     wait_us(10000);
     
-    // release reset to allow the sensor to reboot
+    // release reset and allow 10ms for the sensor to reboot
     gpio0Pin.input();
     wait_us(10000);
-    
+        
     // reset the I2C bus
     i2c.reset();
     
@@ -40,7 +43,7 @@
     t.start();
     while (readReg8(VL6180X_SYSTEM_FRESH_OUT_OF_RESET) != 1)
     {
-        if (t.read_us() > 10000000)
+        if (t.read_us() > 1000000)
             return false;
     }
     
@@ -85,7 +88,10 @@
     
     // allow time to settle
     wait_us(1000);
-    
+        
+    // start the sample timer 
+    sampleTimer.start();
+
     // success
     return true;
 }
@@ -94,20 +100,16 @@
 {
     writeReg8(VL6180X_SYSTEM_GROUPED_PARAMETER_HOLD, 0x01);         // set parameter hold while updating settings
     
-    writeReg8(VL6180X_SYSTEM_INTERRUPT_CONFIG_GPIO, (4<<3) | 4);    // Enable interrupts from range and ambient integrator
-    writeReg8(VL6180X_SYSTEM_MODE_GPIO1, 0x10);                     // Set GPIO1 low when sample complete
+    writeReg8(VL6180X_SYSTEM_INTERRUPT_CONFIG_GPIO, 4);             // Enable interrupts from range only
+    writeReg8(VL6180X_SYSTEM_MODE_GPIO1, 0x00);                     // Disable GPIO1
     writeReg8(VL6180X_SYSRANGE_VHV_REPEAT_RATE, 0xFF);              // Set auto calibration period (Max = 255)/(OFF = 0)
     writeReg8(VL6180X_SYSRANGE_INTERMEASUREMENT_PERIOD, 0x09);      // Set default ranging inter-measurement period to 100ms
-    writeReg8(VL6180X_SYSRANGE_MAX_CONVERGENCE_TIME, 0x32);         // Max range convergence time 48ms
-    writeReg8(VL6180X_SYSRANGE_RANGE_CHECK_ENABLES, 0x11);          // S/N enable, ignore disable, early convergence test enable
-    writeReg16(VL6180X_SYSRANGE_EARLY_CONVERGENCE_ESTIMATE, 0x7B);  // abort range measurement if convergence rate below this value
-
-    writeReg8(VL6180X_SYSALS_INTERMEASUREMENT_PERIOD, 0x0A);        // Set default ALS inter-measurement period to 100ms
-    writeReg8(VL6180X_SYSALS_ANALOGUE_GAIN, 0x46);                  // Set the ALS gain
-    writeReg16(VL6180X_SYSALS_INTEGRATION_PERIOD, 0x63);            // ALS integration time 100ms
-    
-    writeReg8(VL6180X_READOUT_AVERAGING_SAMPLE_PERIOD, 0x30);       // Sample averaging period (1.3ms + N*64.5us)
-    writeReg8(VL6180X_FIRMWARE_RESULT_SCALER, 0x01);
+    writeReg8(VL6180X_SYSRANGE_MAX_CONVERGENCE_TIME, 63);           // Max range convergence time 63ms
+    writeReg8(VL6180X_SYSRANGE_RANGE_CHECK_ENABLES, 0x00);          // S/N disable, ignore disable, early convergence test disable
+    writeReg16(VL6180X_SYSRANGE_EARLY_CONVERGENCE_ESTIMATE, 0x00);  // abort range measurement if convergence rate below this value
+    writeReg8(VL6180X_READOUT_AVERAGING_SAMPLE_PERIOD, averagingSamplePeriod);  // Sample averaging period (1.3ms + N*64.5us)
+    writeReg8(VL6180X_SYSRANGE_THRESH_LOW, 0x00);                   // low threshold
+    writeReg8(VL6180X_SYSRANGE_THRESH_HIGH, 0xff);                  // high threshold
 
     writeReg8(VL6180X_SYSTEM_GROUPED_PARAMETER_HOLD, 0x00);         // end parameter hold
 
@@ -118,7 +120,7 @@
     while (readReg8(VL6180X_SYSRANGE_VHV_RECALIBRATE) != 0)
     {
         // if we've been waiting too long, abort
-        if (t.read_us() > 1000000)
+        if (t.read_us() > 100000)
             break;
     }
 }
@@ -141,25 +143,7 @@
     id.manufDate.mm = (time % 3600) / 60;
     id.manufDate.ss = time % 60;
 }
- 
- 
-uint8_t VL6180X::changeAddress(uint8_t newAddress)
-{  
-    // do nothing if the address is the same or it's out of range
-    if (newAddress == addr || newAddress > 127)
-        return addr;
 
-    // set the new address    
-    writeReg8(VL6180X_I2C_SLAVE_DEVICE_ADDRESS, newAddress);
-    
-    // read it back and store it
-    addr = readReg8(VL6180X_I2C_SLAVE_DEVICE_ADDRESS); 
-    
-    // return the new address
-    return addr;
-}
-  
-  
 void VL6180X::continuousDistanceMode(bool on)
 {
     if (distMode != on)
@@ -184,31 +168,48 @@
 
 bool VL6180X::rangeReady()
 {
-    return (readReg8(VL6180X_RESULT_INTERRUPT_STATUS_GPIO) & 0x07) == 4;
+    // check if the status register says a sample is ready (bits 0-2/0x07)
+    // or an error has occurred (bits 6-7/0xC0)
+    return ((readReg8(VL6180X_RESULT_INTERRUPT_STATUS_GPIO) & 0xC7) != 0);
 }
  
 void VL6180X::startRangeReading()
 {
-    writeReg8(VL6180X_SYSRANGE_START, 0x01);
+    // start a new range reading if one isn't already in progress
+    if (!rangeStarted)
+    {
+        tSampleStart = sampleTimer.read_us();
+        writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
+        writeReg8(VL6180X_SYSRANGE_START, 0x00);
+        writeReg8(VL6180X_SYSRANGE_START, 0x01);
+        rangeStarted = true;
+    }
 }
 
-int VL6180X::getRange(uint8_t &distance, uint32_t timeout_us)
+int VL6180X::getRange(uint8_t &distance, uint32_t &tMid, uint32_t &dt, uint32_t timeout_us)
 {
-    if (!rangeReady())
-        writeReg8(VL6180X_SYSRANGE_START, 0x01);
+    // start a reading if one isn't already in progress
+    startRangeReading();
+    
+    // we're going to wait until this reading ends, so consider the
+    // 'start' command consumed, no matter what happens next
+    rangeStarted = false;
     
     // wait for the sample
     Timer t;
     t.start();
     for (;;)
     {
-        // if the GPIO pin is high, the sample is ready
+        // check for a sample
         if (rangeReady())
             break;
             
         // if we've exceeded the timeout, return failure
         if (t.read_us() > timeout_us)
+        {
+            writeReg8(VL6180X_SYSRANGE_START, 0x00);
             return -1;
+        }
     }
     
     // check for errors
@@ -217,6 +218,22 @@
     // read the distance
     distance = readReg8(VL6180X_RESULT_RANGE_VAL);
     
+    // Read the convergence time, and compute the overall sample time.
+    // Per the data sheet, the total execution time is the sum of the
+    // fixed 3.2ms pre-calculation time, the convergence time, and the
+    // readout averaging time.  We can query the convergence time for
+    // each reading from the sensor.  The averaging time is a controlled
+    // by the READOUT_AVERAGING_SAMPLE_PERIOD setting, which we set to
+    // our constant value averagingSamplePeriod.
+    dt = 
+        3200                                                // fixed 3.2ms pre-calculation period
+        + readReg32(VL6180X_RESULT_RANGE_RETURN_CONV_TIME)  // convergence time
+        + (1300 + 48*averagingSamplePeriod);                // readout averaging period
+        
+    // figure the midpoint of the sample time - the starting time
+    // plus half the collection time
+    tMid = tSampleStart + dt/2;
+    
     // clear the data-ready interrupt
     writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
 
@@ -236,53 +253,13 @@
     stats.refConvTime = readReg32(VL6180X_RESULT_RANGE_REFERENCE_CONV_TIME);
 }
      
-float VL6180X::getAmbientLight(VL6180X_ALS_Gain gain)
-{
-    // set the desired gain
-    writeReg8(VL6180X_SYSALS_ANALOGUE_GAIN, (0x40 | gain));
-    
-    // start the integration
-    writeReg8(VL6180X_SYSALS_START, 0x01);
-
-    // give it time to integrate
-    wait_ms(100);
-    
-    // clear the data-ready interrupt
-    writeReg8(VL6180X_SYSTEM_INTERRUPT_CLEAR, 0x07);
-
-    // retrieve the raw sensor reading om the sensoe
-    unsigned int alsRaw = readReg16(VL6180X_RESULT_ALS_VAL);
-    
-    // get the integration period
-    unsigned int tIntRaw = readReg16(VL6180X_SYSALS_INTEGRATION_PERIOD);
-    float alsIntegrationPeriod = 100.0 / tIntRaw ;
-    
-    // get the actual gain at the user's gain setting
-    float trueGain = 0.0;
-    switch (gain)
-    {
-    case GAIN_20:   trueGain = 20.0; break;
-    case GAIN_10:   trueGain = 10.32; break;
-    case GAIN_5:    trueGain = 5.21; break;
-    case GAIN_2_5:  trueGain = 2.60; break;
-    case GAIN_1_67: trueGain = 1.72; break;
-    case GAIN_1_25: trueGain = 1.28; break;
-    case GAIN_1:    trueGain = 1.01; break;
-    case GAIN_40:   trueGain = 40.0; break;
-    default:        trueGain = 1.0;  break;
-    }
-    
-    // calculate the lux (see the manufacturer's app notes)
-    return alsRaw  * 0.32f / trueGain * alsIntegrationPeriod;
-}
- 
 uint8_t VL6180X::readReg8(uint16_t registerAddr)
 {
     // write the request - MSB+LSB of register address
     uint8_t data_write[2];
     data_write[0] = (registerAddr >> 8) & 0xFF;
     data_write[1] = registerAddr & 0xFF;
-    if (i2c.write(addr << 1, data_write, 2, true))
+    if (i2c.write(addr << 1, data_write, 2, false))
         return 0x00;
 
     // read the result
@@ -300,7 +277,7 @@
     uint8_t data_write[2];
     data_write[0] = (registerAddr >> 8) & 0xFF;
     data_write[1] = registerAddr & 0xFF;
-    if (i2c.write(addr << 1, data_write, 2, true))
+    if (i2c.write(addr << 1, data_write, 2, false))
         return 0;
     
     // read the result
--- a/VL6180X/VL6180X.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/VL6180X/VL6180X.h	Tue May 09 05:48:37 2017 +0000
@@ -7,23 +7,6 @@
 #include "BitBangI2C.h"
 
 
-class MyI2C: public I2C
-{
-public:
-    MyI2C(PinName sda, PinName scl) : I2C(sda, scl) { }
-    
-    int write(int addr, const uint8_t *data, size_t len, bool repeated = false)
-    {
-        return I2C::write(addr, (const char *)data, len, repeated);
-    }
-    int read(int addr, uint8_t *data, size_t len, bool repeated = false)
-    {
-        return I2C::read(addr, (char *)data, len, repeated);
-    }
-    
-    void reset() { }
-};
-
 #define VL6180X_IDENTIFICATION_MODEL_ID              0x0000
 #define VL6180X_IDENTIFICATION_MODEL_REV_MAJOR       0x0001
 #define VL6180X_IDENTIFICATION_MODEL_REV_MINOR       0x0002
@@ -135,12 +118,19 @@
 {
 public:
     // Set up the interface with the given I2C pins, I2C address, and
-    // the GPIO0 pin (for resetting the sensor at startup).
+    // the GPIO0 pin (for resetting the sensor at startup).  
+    //
+    // If 'internalPullups' is true, we'll set the I2C SDA/SCL pins to 
+    // enable the internal pullup resistors.  Set this to false if you're
+    // using your own external pullup resistors on the lines.  External
+    // pullups are better if you're attaching more than one device to the
+    // same I2C bus; the internal pullups are fine for a single device.
     //
     // Note that the power-on default I2C address is always 0x29.  The
     // address can be changed during a session, but it's not saved 
     // persistently; it always resets to 0x29 on the next power cycle.
-    VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0);
+    VL6180X(PinName sda, PinName scl, uint8_t addr, PinName gpio0,
+        bool internalPullups);
     
     // destruction
     ~VL6180X();
@@ -159,7 +149,15 @@
 
     // Get TOF range distance in mm.  Returns 0 on success, a device
     // "range error code" (>0) on failure, or -1 on timeout.
-    int getRange(uint8_t &distance, uint32_t timeout_us);
+    //
+    // 'tMid' is the timestamp in microseconds of the midpoint of the
+    // sample, relative to an arbitrary zero point.  This can be used
+    // to construct a timeline of successive readings, such as for
+    // velocity calculations.  'dt' is the time the sensor took to
+    // collect the sample.
+    int getRange(
+        uint8_t &distance, uint32_t &tMid, uint32_t &dt, 
+        uint32_t timeout_us);
     
     // get range statistics
     void getRangeStats(VL6180X_RangeStats &stats);
@@ -170,17 +168,15 @@
     // is a sample ready?
     bool rangeReady();
 
-    // get ambient light level in lux
-    float getAmbientLight(VL6180X_ALS_Gain gain);
-
     // get identification data
     void getID(VL6180X_ID &id);
 
-    // Change the address of the device.  Returns the new address.
-    uint8_t changeAddress(uint8_t newAddress);
- 
+protected:
+    // READOUT_AVERAGING_SAMPLE_PERIOD setting.  Each unit represents
+    // 64.5us of added time beyond the 1.3ms fixed base period.  The
+    // default is 48 units.
+    static const int averagingSamplePeriod = 48;
 
-protected:
     // I2C interface to device
     BitBangI2C i2c;
     
@@ -192,6 +188,15 @@
     
     // current distance mode: 0=single shot, 1=continuous
     bool distMode;
+    
+    // range reading is in progress
+    bool rangeStarted;
+    
+    // sample timer
+    Timer sampleTimer;
+
+    // time (from Timer t) of start of last range sample
+    uint32_t tSampleStart;
 
     // read registers
     uint8_t readReg8(uint16_t regAddr);
--- a/cfgVarMsgMap.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/cfgVarMsgMap.h	Tue May 09 05:48:37 2017 +0000
@@ -48,7 +48,7 @@
         
         // ********** DESCRIBE CONFIGURATION VARIABLES **********
     case 0:
-        v_byte_ro(19, 2);       // number of SCALAR variables
+        v_byte_ro(21, 2);       // number of SCALAR variables
         v_byte_ro(6, 3);        // number of ARRAY variables
         break;
         
@@ -185,6 +185,18 @@
         v_ui16(plunger.jitterWindow, 2);
         break;
         
+    case 20:
+        // bar-code plunger setup
+        v_ui16(plunger.barCode.startPix, 2);
+        break;
+        
+    case 21:
+        v_ui16(tlc59116.chipMask, 2);
+        v_byte(tlc59116.sda, 4);
+        v_byte(tlc59116.scl, 5);
+        v_byte(tlc59116.reset, 6);
+        break;
+        
     // case N: // new scalar variable
     //
     // !!! ATTENTION !!!
--- a/config.h	Fri Apr 21 18:50:37 2017 +0000
+++ b/config.h	Tue May 09 05:48:37 2017 +0000
@@ -1,49 +1,44 @@
 // Pinscape Controller Configuration
 //
-// New for 2016:  dynamic configuration!  To configure the controller,
-// connect the KL25Z to your PC, install the STANDARD pre-compiled .bin 
-// file, and run the Windows config tool.  There's no need (as there was in 
-// the past) to edit the source code or to compile a custom version of the 
-// binary just to customize setup options.
+// !!! ATTENTION !!! 
+// If you've come here on advice in a forum to change a GPIO setting or 
+// to #define a macro to enable the expansion boards, >>>STOP NOW<<<.  The 
+// advice you found is out of date and no longer applies.  You don't need 
+// to edit this file or recompile the firmware, and you shouldn't.  Instead,
+// use the standard firmware, and set options using the Pinscape Config Tool
+// on your Windows PC.  All options that were formerly configurable by 
+// editing this file can be selected with the Config Tool.  That's much 
+// cleaner and easier than editing the source code, and it eliminates the
+// problem of re-synchronizing a private copy of the source code with future 
+// updates.  With the config tool, you only need the standard firmware build, 
+// so future updates are a simple matter of downloading the latest version.
 //
-// In earlier versions, configuration was handled mostly with #ifdef and
-// similar constructs.  To customize the setup, you had to create a private 
-// forked copy of the source code, edit the constants defined in config.h, 
-// and compile a custom binary.  That's no longer necessary!
 //
-// The new approach is to do everything (or as much as possible, anyway)
-// via the Windows config tool.  You shouldn't have to recompile a custom
-// version just to make a configurable change.  Of course, you're still free
-// to create a custom version if you want to add entirely new features or 
-// make changes that go beyond what the setup tool exposes.
+// IN THE PAST (but NOT NOW - see above), configuration was handled mostly 
+// with #defines and #ifdefs.  To customize the setup, you had to create a 
+// private forked copy of the source code, edit the constants defined in 
+// config.h, and compile a custom binary.  That's no longer necessary because
+// the config tool lets you set all configurable options dynamically.  Of 
+// course, you're still free to create a custom version if you want to add 
+// entirely new features or make changes that go beyond the configurable
+// options.
 //
+#ifndef CONFIG_H
+#define CONFIG_H
 
-// Pre-packaged configuration selection.
+
+// TEST SETTINGS - FOR DEBUGGING PURPOSES ONLY.  The macros below select
+// special option combinations for debugging purposes.
 //
-// IMPORTANT!  If you just want to create a custom configuration, DON'T
-// modify this file, DON'T use these macros, and DON'T compiler on mbed.
-// Instead, use the unmodified standard build and configure your system
-// using the Pinscape Config Tool on Windows.  That's easier and better
-// because the config tool will be able to back up your settings to a
-// local file on your PC, and will automatically preserve your settings
-// across upgrades.  You won't have to worry about merging your changes
-// into every update of the repository source code, since you'll never
-// have to change the source code.
-//
-// The different configurations here are purely for testing purposes.  
-// The standard build uses the STANDARD_CONFIG settings, which are the 
-// same as the original version where you had to modify config.h by hand 
-// to customize your system.
-// 
+// IMPORTANT!  If you're trying to create a custom configuration because
+// you have a pin conflict or because you're using the expansion boards,
+// DON'T modify this file, DON'T use these macros, and DON'T recompile 
+// the firmware.  Use the Config Tool on your Windows PC instead.
 #define STANDARD_CONFIG       1     // standard settings, based on v1 base settings
 #define TEST_CONFIG_EXPAN     0     // configuration for the expansion boards
 #define TEST_KEEP_PRINTF      0     // for debugging purposes, keep printf() enabled
                                     // by leaving the SDA UART GPIO pins unallocated
 
-
-#ifndef CONFIG_H
-#define CONFIG_H
-
 // Plunger type codes
 // NOTE!  These values are part of the external USB interface.  New
 // values can be added, but the meaning of an existing assigned number 
@@ -143,6 +138,7 @@
 const int PortType74HC595      = 4;      // 74HC595 port
 const int PortTypeVirtual      = 5;      // Virtual port - visible to host software, but not connected 
                                          //  to a physical output
+const int PortTypeTLC59116     = 6;      // TLC59116 port
 
 // LedWiz output port flag bits
 const uint8_t PortFlagActiveLow  = 0x01; // physical output is active-low
@@ -156,10 +152,22 @@
 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 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 in the overall daisy chain, starting 
+                        //    from 0 for OUT0 on the first chip in the chain
+                        //
+                        //  - for a TLC59116, the high 4 bits are the chip
+                        //    address (the low 4 bits of the address only),
+                        //    and the low 4 bits are the output number on
+                        //    the chip
+                        //
+                        //  - for inactive and virtual ports, this is unused
+                        //
     uint8_t flags;      // flags:  a combination of PortFlagXxx values
     
     void set(uint8_t typ, uint8_t pin, uint8_t flags = 0)
@@ -331,6 +339,13 @@
         hc595.latch = PINNAME_TO_WIRE(PTA12);
         hc595.ena = PINNAME_TO_WIRE(PTD4);
         
+        // disable all TLC59116 chips by default
+        tlc59116.chipMask = 0;
+        
+        // Default TLC59116 pin assignments
+        tlc59116.sda = PINNAME_TO_WIRE(PTC6);
+        tlc59116.scl = PINNAME_TO_WIRE(PTC5);
+        tlc59116.reset = PINNAME_TO_WIRE(PTC10);
         
         // Default IR hardware pin assignments.  On the expansion boards,
         // the sensor is connected to PTA13, and the emitter LED is on PTC9.
@@ -618,6 +633,12 @@
         // physical travel.  Zero disables the jitter filter.
         uint16_t jitterWindow;
         
+        // bar code sensor parameters
+        struct
+        {
+            uint16_t startPix;  // starting pixel offset
+        } barCode;
+        
         // ZB LAUNCH BALL button setup.
         //
         // This configures the "ZB Launch Ball" feature in DOF, based on Zeb's (of 
@@ -757,7 +778,7 @@
     struct
     {
         // number of TLC5940NT chips connected in daisy chain
-        int nchips;
+        uint8_t nchips;
         
         // pin connections (wire pin IDs)
         uint8_t sin;        // Serial data - must connect to SPIO MOSI -> PTC6 or PTD2
@@ -774,7 +795,7 @@
     struct
     {
         // number of 74HC595 chips attached in daisy chain
-        int nchips;
+        uint8_t nchips;
         
         // pin connections
         uint8_t sin;        // Serial data - use any GPIO pin
@@ -784,6 +805,21 @@
     
     } hc595;
     
+    // --- TLC59116 PWM Controller Chip Setup --
+    struct
+    {
+        // Chip mask.  Each bit represents an enabled chip at the
+        // corresponding 4-bit address (i.e., bit 1<<addr represents
+        // the chip at 'addr').
+        uint16_t chipMask;
+        
+        // pin connections
+        uint8_t sda;        // I2C SDA
+        uint8_t scl;        // I2C SCL
+        uint8_t reset;      // !RESET (hardware reset line, active low)
+        
+    } tlc59116;
+    
     
     // --- IR Remote Control Hardware Setup ---
     struct
--- a/main.cpp	Fri Apr 21 18:50:37 2017 +0000
+++ b/main.cpp	Tue May 09 05:48:37 2017 +0000
@@ -47,34 +47,48 @@
 //    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.
+//    We support several sensor types:
 //
-//    Note that 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.
+//    - AEDR-8300-1K2 optical encoders.  These are quadrature encoders with
+//      reflective optical sensing and built-in lighting and optics.  The sensor
+//      is attached to the plunger so that it moves with the plunger, and slides
+//      along a guide rail with a reflective pattern of regularly spaces bars 
+//      for the encoder to read.  We read the plunger position by counting the
+//      bars the sensor passes as it moves across the rail.  This is the newest
+//      option, and it's my current favorite because it's highly accurate,
+//      precise, and fast, plus it's relatively inexpensive.
+//
+//    - Slide potentiometers.  There are slide potentioneters available with a
+//      long enough travel distance (at least 85mm) to cover the plunger travel.
+//      Attach the plunger to the potentiometer knob so that the moving the
+//      plunger moves the pot knob.  We sense the position by simply reading
+//      the analog voltage on the pot brush.  A pot with a "linear taper" (that
+//      is, the resistance varies linearly with the position) is required.
+//      This option is cheap, easy to set up, and works well.
 //
-//    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
-//    to do the calibration once, when you first install everything.  (You might
-//    also want to re-calibrate if you physically remove and reinstall the CCD 
-//    sensor or the mechanical plunger, since their alignment shift change slightly 
-//    when you put everything back together.)  You can optionally install a
-//    dedicated momentary switch or pushbutton to activate the calibration mode;
-//    this is describe in the project documentation.  If you don't want to bother
-//    with the extra button, you can also trigger calibration using the Windows 
-//    setup software, which you can find on the Pinscape project page.
+//    - VL6108X time-of-flight distance sensor.  This is an optical distance
+//      sensor that measures the distance to a nearby object (within about 10cm)
+//      by measuring the travel time for reflected pulses of light.  It's fairly
+//      cheap and easy to set up, but I don't recommend it because it has very
+//      low precision.
 //
-//    The calibration procedure is described in the project documentation.  Briefly,
-//    when you trigger calibration mode, the software will scan the CCD for about
-//    15 seconds, during which you should simply pull the physical plunger back
-//    all the way, hold it for a moment, and then slowly return it to the rest
-//    position.  (DON'T just release it from the retracted position, since that
-//    let it shoot forward too far.  We want to measure the range from the park
-//    position to the fully retracted position only.)
+//    - TSL1410R/TSL1412R linear array optical sensors.  These are large optical
+//      sensors with the pixels arranged in a single row.  The pixel arrays are
+//      large enough on these to cover the travel distance of the plunger, so we
+//      can set up the sensor near the plunger in such a way that the plunger 
+//      casts a shadow on the sensor.  We detect the plunger position by finding
+//      the edge of the sahdow in the image.  The optics for this setup are very
+//      simple since we don't need any lenses.  This was the first sensor we
+//      supported, and works very well, but unfortunately the sensor is difficult
+//      to find now since it's been discontinued by the manufacturer.
+//
+//    The v2 Build Guide has details on how to build and configure all of the
+//    sensor options.
+//
+//    Visual Pinball has built-in support for plunger devices like this one, but 
+//    some older VP tables (particularly for VP 9) can't use it without some
+//    modifications to their scripting.  The Build Guide has advice on how to
+//    fix up VP tables to add plunger support when necessary.
 //
 //  - Button input wiring.  You can assign GPIO ports as inputs for physical
 //    pinball-style buttons, such as flipper buttons, a Start button, coin
@@ -107,28 +121,34 @@
 //    current handing.  The Build Guide has a reference circuit design for this
 //    purpose that's simple and inexpensive to build.
 //
-//  - Enhanced LedWiz emulation with TLC5940 PWM controller chips.  You can attach
-//    external PWM controller chips for controlling device outputs, instead of using
-//    the on-board GPIO ports as described above.  The software can control a set of 
-//    daisy-chained TLC5940 chips.  Each chip provides 16 PWM outputs, so you just
-//    need two of them to get the full complement of 32 output ports of a real LedWiz.
-//    You can hook up even more, though.  Four chips gives you 64 ports, which should
-//    be plenty for nearly any virtual pinball project.  To accommodate the larger
-//    supply of ports possible with the PWM chips, the controller software provides
-//    a custom, extended version of the LedWiz protocol that can handle up to 128
-//    ports.  PC software designed only for the real LedWiz obviously won't know
-//    about the extended protocol and won't be able to take advantage of its extra
-//    capabilities, but the latest version of DOF (DirectOutput Framework) *does* 
-//    know the new language and can take full advantage.  Older software will still
-//    work, though - the new extensions are all backward compatible, so old software
-//    that only knows about the original LedWiz protocol will still work, with the
-//    obvious limitation that it can only access the first 32 ports.
+//  - Enhanced LedWiz emulation with TLC5940 and/or TLC59116 PWM controller chips. 
+//    You can attach external PWM chips for controlling device outputs, instead of
+//    using (or in addition to) the on-board GPIO ports as described above.  The 
+//    software can control a set of daisy-chained TLC5940 or TLC59116 chips.  Each
+//    chip provides 16 PWM outputs, so you just need two of them to get the full 
+//    complement of 32 output ports of a real LedWiz.  You can hook up even more, 
+//    though.  Four chips gives you 64 ports, which should be plenty for nearly any 
+//    virtual pinball project.  
 //
 //    The Pinscape Expansion Board project (which appeared in early 2016) provides
 //    a reference hardware design, with EAGLE circuit board layouts, that takes full
 //    advantage of the TLC5940 capability.  It lets you create a customized set of
 //    outputs with full PWM control and power handling for high-current devices
-//    built in to the boards.  
+//    built in to the boards.
+//
+//    To accommodate the larger supply of ports possible with the external chips,
+//    the controller software provides a custom, extended version of the LedWiz 
+//    protocol that can handle up to 128 ports.  Legacy PC software designed only
+//    for the original LedWiz obviously can't use the extended protocol, and thus 
+//    can't take advantage of its extra capabilities, but the latest version of 
+//    DOF (DirectOutput Framework) *does*  know the new language and can take full
+//    advantage.  Older software will still work, though - the new extensions are 
+//    all backwards compatible, so old software that only knows about the original 
+//    LedWiz protocol will still work, with the limitation that it can only access 
+//    the first 32 ports.  In addition, we provide a replacement LEDWIZ.DLL that 
+//    creates virtual LedWiz units representing additional ports beyond the first
+//    32.  This allows legacy LedWiz client software to address all ports by
+//    making them think that you have several physical LedWiz units installed.
 //
 //  - Night Mode control for output devices.  You can connect a switch or button
 //    to the controller to activate "Night Mode", which disables feedback devices
@@ -222,6 +242,7 @@
 #include "FreescaleIAP.h"
 #include "crc32.h"
 #include "TLC5940.h"
+#include "TLC59116.h"
 #include "74HC595.h"
 #include "nvm.h"
 #include "TinyDigitalIn.h"
@@ -237,6 +258,7 @@
 #include "nullSensor.h"
 #include "barCodeSensor.h"
 #include "distanceSensor.h"
+#include "tsl14xxSensor.h"
 
 
 #define DECL_EXTERNS
@@ -646,17 +668,26 @@
 // about 50 GPIO pins.  So if you want to do everything with GPIO ports,
 // you have to ration pins among features.
 //
-// To overcome some of these limitations, we also provide two types of
+// To overcome some of these limitations, we also support several external
 // peripheral controllers that allow adding many more outputs, using only
-// a small number of GPIO pins to interface with the peripherals.  First,
-// we support TLC5940 PWM controller chips.  Each TLC5940 provides 16 ports
-// with full PWM, and multiple TLC5940 chips can be daisy-chained.  The
-// chip only requires 5 GPIO pins for the interface, no matter how many
-// chips are in the chain, so it effectively converts 5 GPIO pins into 
-// almost any number of PWM outputs.  Second, we support 74HC595 chips.
-// These provide only digital outputs, but like the TLC5940 they can be
-// daisy-chained to provide almost unlimited outputs with a few GPIO pins
-// to control the whole chain.
+// a small number of GPIO pins to interface with the peripherals:
+//
+// - TLC5940 PWM controller chips.  Each TLC5940 provides 16 ports with
+//   12-bit PWM, and multiple TLC5940 chips can be daisy-chained.  The
+//   chips connect via 5 GPIO pins, and since they're daisy-chainable,
+//   one set of 5 pins can control any number of the chips.  So this chip
+//   effectively converts 5 GPIO pins into almost any number of PWM outputs.
+//
+// - TLC59116 PWM controller chips.  These are similar to the TLC5940 but
+//   a newer generation with an improved design.  These use an I2C bus,
+//   allowing up to 14 chips to be connected via 3 GPIO pins.
+//
+// - 74HC595 shift register chips.  These provide 8 digital (on/off only)
+//   outputs per chip.  These need 4 GPIO pins, and like the other can be
+//   daisy chained to add more outputs without using more GPIO pins.  These
+//   are advantageous for outputs that don't require PWM, since the data
+//   transfer sizes are so much smaller.  The expansion boards use these
+//   for the chime board outputs.
 //
 // Direct GPIO output ports and peripheral controllers can be mixed and
 // matched in one system.  The assignment of pins to ports and the 
@@ -735,7 +766,7 @@
 
 
 // Gamma correction table for 8-bit input values
-static const uint8_t gamma[] = {
+static const uint8_t dof_to_gamma_8bit[] = {
       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 
       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   1, 
       1,   1,   1,   1,   1,   1,   1,   1,   1,   2,   2,   2,   2,   2,   2,   2, 
@@ -761,7 +792,7 @@
 {
 public:
     LwGammaOut(LwOut *o) : out(o) { }
-    virtual void set(uint8_t val) { out->set(gamma[val]); }
+    virtual void set(uint8_t val) { out->set(dof_to_gamma_8bit[val]); }
     
 private:
     LwOut *out;
@@ -903,10 +934,55 @@
     uint8_t prv;
 };
 
-
-
+//
+// TLC59116 interface object
+//
+TLC59116 *tlc59116 = 0;
+void init_tlc59116(Config &cfg)
+{
+    // Create the interface if any chips are enabled
+    if (cfg.tlc59116.chipMask != 0)
+    {
+        // set up the interface
+        tlc59116 = new TLC59116(
+            wirePinName(cfg.tlc59116.sda),
+            wirePinName(cfg.tlc59116.scl),
+            wirePinName(cfg.tlc59116.reset));
+            
+        // initialize the chips
+        tlc59116->init();
+    }
+}
+
+// LwOut class for TLC59116 outputs.  The 'addr' value in the constructor
+// is low 4 bits of the chip's I2C address; this is the part of the address
+// that's configurable per chip.  'port' is the output number on the chip
+// (0-15).
+//
+// Note that we don't need a separate gamma-corrected subclass for this
+// output type, since there's no loss of precision with the standard layered
+// gamma (it emits 8-bit values, and we take 8-bit inputs).
+class Lw59116Out: public LwOut
+{
+public:
+    Lw59116Out(uint8_t addr, uint8_t port) : addr(addr), port(port) { prv = 0; }
+    virtual void set(uint8_t val)
+    {
+        if (val != prv)
+            tlc59116->set(addr, port, prv = val);
+    }
+    
+protected:
+    uint8_t addr;
+    uint8_t port;
+    uint8_t prv;
+};
+
+
+//
 // 74HC595 interface object.  Set this up with the port assignments in
 // config.h.
+//
 HC595 *hc595 = 0;
 
 // initialize the 74HC595 interface
@@ -1276,13 +1352,22 @@
         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)
+        // 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 PortTypeTLC59116:
+        // TLC59116 port.  The pin number in the config encodes the chip address
+        // in the high 4 bits and the output number on the chip in the low 4 bits.
+        // There's no gamma-corrected version of this output handler, so we don't
+        // need to worry about that here; just use the layered gamma as needed.
+        if (tlc59116 != 0)
+            lwp = new Lw59116Out((pin >> 4) & 0x0F, pin & 0x0F);
+        break;
 
     case PortTypeVirtual:
     case PortTypeDisabled:
@@ -3907,10 +3992,8 @@
         else
         {
             // The latch didn't stick, so PSU2 was still off at
-            // our last check.  Try pulsing it again in case PSU2
-            // was turned on since the last check.
-            psu2_status_set->write(1);
-            psu2_state = 2;
+            // our last check.  Return to idle state.
+            psu2_state = 1;
         }
         break;
         
@@ -4197,6 +4280,8 @@
         saveConfigSucceededFlag = 0x40;
             
         // start the followup timer
+        saveConfigFollowupTime = tFollowup;
+        saveConfigFollowupTimer.reset();
         saveConfigFollowupTimer.start();
         
         // if a reboot is pending, flag it
@@ -4336,10 +4421,10 @@
 // the plunger sensor interface object
 PlungerSensor *plungerSensor = 0;
 
-// wait for the plunger sensor to complete any outstanding read
+// wait for the plunger sensor to complete any outstanding DMA transfer
 static void waitPlungerIdle(void)
 {
-    while (!plungerSensor->ready()) { }
+    while (plungerSensor->dmaBusy()) { }
 }
 
 // Create the plunger sensor based on the current configuration.  If 
@@ -4409,8 +4494,9 @@
         break;
     }
     
-    // set the jitter filter
-    plungerSensor->setJitterWindow(cfg.plunger.jitterWindow);
+    // initialize the config variables affecting the plunger
+    plungerSensor->onConfigChange(19, cfg);
+    plungerSensor->onConfigChange(20, cfg);
 }
 
 // Global plunger calibration mode flag
@@ -4473,9 +4559,6 @@
     {
         // not in a firing event yet
         firing = 0;
-
-        // no history yet
-        histIdx = 0;
     }
     
     // Collect a reading from the plunger sensor.  The main loop calls
@@ -4492,25 +4575,6 @@
         PlungerReading r;
         if (plungerSensor->read(r))
         {
-            // Pull the previous reading from the history
-            const PlungerReading &prv = nthHist(0);
-         
-            // If the new reading is within 1ms of the previous reading,
-            // ignore it.  We require a minimum time between samples to
-            // ensure that we have a usable amount of precision in the
-            // denominator (the time interval) for calculating the plunger
-            // velocity.  The CCD sensor hardware takes about 2.5ms to
-            // read, so it will never be affected by this, but other sensor
-            // types don't all have the same hardware cycle time, so we need
-            // to throttle them artificially.  E.g., the potentiometer only
-            // needs one ADC sample per reading, which only takes about 15us;
-            // the quadrature sensor needs no time at all since it keeps
-            // track of the position continuously via interrupts.  We don't
-            // need to check which sensor type we have here; we just ignore 
-            // readings until the minimum interval has passed.
-            if (uint32_t(r.t - prv.t) < 1000UL)
-                return;
-
             // check for calibration mode
             if (plungerCalMode)
             {
@@ -4575,98 +4639,130 @@
                     r.pos = -JOYMAX;
             }
 
-            // Calculate the velocity from the second-to-last reading
-            // to here, in joystick distance units per microsecond.
-            // Note that we use the second-to-last reading rather than
-            // the very last reading to give ourselves a little longer
-            // time base.  The time base is so short between consecutive
-            // readings that the error bars in the position would be too
-            // large.
+            // Look for a firing event - the user releasing the plunger and
+            // allowing it to shoot forward at full speed.  Wait at least 5ms
+            // between samples for this, to help distinguish random motion 
+            // from the rapid motion of a firing event.  
             //
-            // For reference, the physical plunger velocity ranges up
-            // to about 100,000 joystick distance units/sec.  This is 
-            // based on empirical measurements.  The typical time for 
-            // a real plunger to travel the full distance when released 
-            // from full retraction is about 85ms, so the average velocity 
-            // covering this distance is about 56,000 units/sec.  The 
-            // peak is probably about twice that.  In real-world units, 
-            // this translates to an average speed of about .75 m/s and 
-            // a peak of about 1.5 m/s.
+            // There's a trade-off in the choice of minimum sampling interval.
+            // The longer we wait, the more certain we can be of the trend.
+            // But if we wait too long, the user will perceive a delay.  We
+            // also want to sample frequently enough to see the release motion
+            // at intermediate steps along the way, so the sampling has to be
+            // considerably faster than the whole travel time, which is about
+            // 25-50ms.
+            if (uint32_t(r.t - prv.t) < 5000UL)
+                return;
+                
+            // assume that we'll report this reading as-is
+            z = r.pos;
+                
+            // Firing event detection.
+            //
+            // A "firing event" is when the player releases the plunger from
+            // a retracted position, allowing it to shoot forward under the
+            // spring tension.
             //
-            // Note that we actually calculate the value here in units
-            // per *microsecond* - the discussion above is in terms of
-            // units/sec because that's more on a human scale.  Our
-            // choice of internal units here really isn't important,
-            // since we only use the velocity for comparison purposes,
-            // to detect acceleration trends.  We therefore save ourselves
-            // a little CPU time by using the natural units of our inputs.
+            // We monitor the plunger motion for these events, and when they
+            // occur, we report an "idealized" version of the motion to the
+            // PC.  The idealized version consists of a series of readings
+            // frozen at the fully retracted position for the whole duration 
+            // of the forward travel, followed by a series of readings at the
+            // fully forward position for long enough for the plunger to come
+            // mostly to rest.  The series of frozen readings aren't meant to
+            // be perceptible to the player - we try to keep them short enough
+            // that they're not apparent as delay.  Instead, they're for the
+            // PC client software's benefit.  PC joystick clients use polling,
+            // so they only see an unpredictable subset of the readings we
+            // send.  The only way to be sure that the client sees a particular 
+            // reading is to hold it for long enough that the client is sure to
+            // poll within the hold interval.  In the case of the plunger 
+            // firing motion, it's important that the client sees the *ends*
+            // of the travel - the fully retracted starting position in
+            // particular.  If the PC client only polls for a sample while the
+            // plunger is somewhere in the middle of the travel, the PC will
+            // think that the firing motion *started* in that middle position,
+            // so it won't be able to model the right amount of momentum when
+            // the plunger hits the ball.  We try to ensure that the PC sees
+            // the right starting point by reporting the starting point for 
+            // extra time during the forward motion.  By the same token, we
+            // want the PC to know that the plunger has moved all the way
+            // forward, rather than mistakenly thinking that it stopped
+            // somewhere in the middle of the travel, so we freeze at the
+            // forward position for a short time.
             //
-            // We don't care about the absolute velocity; this is a purely
-            // relative calculation.  So to speed things up, calculate it
-            // in the integer domain, using a fixed-point representation
-            // with a 64K scale.  In other words, with the stored values
-            // shifted left 16 bits from the actual values: the value 1
-            // is stored as 1<<16.  The position readings are in the range
-            // -JOYMAX..JOYMAX, which fits in 16 bits, and the time 
-            // differences will generally be on the scale of a few 
-            // milliseconds = thousands of microseconds.  So the velocity
-            // figures will fit nicely into a 32-bit fixed point value with
-            // a 64K scale factor.
-            const PlungerReading &prv2 = nthHist(1);
-            int v = ((r.pos - prv2.pos) * 65536L)/int(r.t - prv2.t);
-            
-            // presume we'll report the latest instantaneous reading
-            z = r.pos;
-            
-            // Check firing events
+            // To detect a firing event, we look for forward motion that's
+            // fast enough to be a firing event.  To determine how fast is
+            // fast enough, we use a simple model of the plunger motion where 
+            // the acceleration is constant.  This is only an approximation, 
+            // as the spring force actually varies with spring's compression, 
+            // but it's close enough for our purposes here.
+            //
+            // Do calculations in fixed-point 2^48 scale with 64-bit ints.
+            // acc2 = acceleration/2 for 50ms release time, units of unit
+            // distances per microsecond squared, where the unit distance
+            // is the overall travel from the starting retracted position
+            // to the park position.
+            const int32_t acc2 = 112590;  // 2^48 scale
             switch (firing)
             {
             case 0:
-                // Default state - not in a firing event.  
-                
-                // If we have forward motion from a position that's retracted 
-                // beyond a threshold, enter phase 1.  If we're not pulled back
-                // far enough, don't bother with this, as a release wouldn't
-                // be strong enough to require the synthetic firing treatment.
-                if (v < 0 && r.pos > JOYMAX/6)
+                // Not in firing mode.  If we're retracted a bit, and the
+                // motion is forward at a fast enough rate to look like a
+                // release, enter firing mode.
+                if (r.pos > JOYMAX/6)
                 {
-                    // enter firing phase 1
-                    firingMode(1);
-                    
-                    // if in calibration state 1 (at rest), switch to state 2 (not 
-                    // at rest)
-                    if (calState == 1)
-                        calState = 2;
-                    
-                    // we don't have a freeze position yet, but note the start time
-                    f1.pos = 0;
-                    f1.t = r.t;
-                    
-                    // Figure the barrel spring "bounce" position in case we complete 
-                    // the firing event.  This is the amount that the forward momentum
-                    // of the plunger will compress the barrel spring at the peak of
-                    // the forward travel during the release.  Assume that this is
-                    // linearly proportional to the starting retraction distance.  
-                    // The barrel spring is about 1/6 the length of the main spring, 
-                    // so figure it compresses by 1/6 the distance.  (This is overly
-                    // simplistic and not very accurate, but it seems to give good 
-                    // visual results, and that's all it's for.)
-                    f2.pos = -r.pos/6;
+                    const uint32_t dt = uint32_t(r.t - prv.t);
+                    const uint32_t dt2 = dt*dt;  // dt^2
+                    if (r.pos < prv.pos - int((prv.pos*acc2*uint64_t(dt2)) >> 48))
+                    {
+                        // Tentatively enter firing mode.  Use the prior reading
+                        // as the starting point, and freeze reports for now.
+                        firingMode(1);
+                        f0 = prv;
+                        z = f0.pos;
+
+                        // if in calibration state 1 (at rest), switch to 
+                        // state 2 (not at rest)
+                        if (calState == 1)
+                            calState = 2;
+                    }
                 }
                 break;
                 
             case 1:
-                // Phase 1 - acceleration.  If we cross the zero point, trigger
-                // the firing event.  Otherwise, continue monitoring as long as we
-                // see acceleration in the forward direction.
+                // Tentative firing mode: the plunger was moving forward
+                // at last check.  To stay in firing mode, the plunger has
+                // to keep moving forward fast enough to look like it's 
+                // moving under spring force.  To figure out how fast is
+                // fast enough, we use a simple model where the acceleration
+                // is constant over the whole travel distance and the total
+                // travel time is 50ms.  The acceleration actually varies
+                // slightly since it comes from the spring force, which
+                // is linear in the displacement; but the plunger spring is
+                // fairly compressed even when the plunger is all the way
+                // forward, so the difference in tension from one end of
+                // the travel to the other is fairly small, so it's not too
+                // far off to model it as constant.  And the real travel
+                // time obviously isn't a constant, but all we need for 
+                // that is an upper bound.  So: we'll figure the time since 
+                // we entered firing mode, and figure the distance we should 
+                // have traveled to complete the trip within the maximum
+                // time allowed.  If we've moved far enough, we'll stay
+                // in firing mode; if not, we'll exit firing mode.  And if
+                // we cross the finish line while still in firing mode,
+                // we'll switch to the next phase of the firing event.
                 if (r.pos <= 0)
                 {
-                    // switch to the synthetic firing mode
+                    // We crossed the park position.  Switch to the second
+                    // phase of the firing event, where we hold the reported
+                    // position at the "bounce" position (where the plunger
+                    // is all the way forward, compressing the barrel spring).
+                    // We'll stick here long enough to ensure that the PC
+                    // client (Visual Pinball or whatever) sees the reading
+                    // and processes the release motion via the simulated
+                    // physics.
                     firingMode(2);
-                    z = f2.pos;
-                    
-                    // note the start time for the firing phase
-                    f2.t = r.t;
                     
                     // if in calibration mode, and we're in state 2 (moving), 
                     // collect firing statistics for calibration purposes
@@ -4676,142 +4772,96 @@
                         // come to rest
                         calState = 0;
                         
-                        // collect average firing time statistics in millseconds, if 
-                        // it's in range (20 to 255 ms)
-                        int dt = uint32_t(r.t - f1.t)/1000UL;
-                        if (dt >= 20 && dt <= 255)
+                        // collect average firing time statistics in millseconds,
+                        // if it's in range (20 to 255 ms)
+                        const int dt = uint32_t(r.t - f0.t)/1000UL;
+                        if (dt >= 15 && dt <= 255)
                         {
                             calRlsTimeSum += dt;
                             calRlsTimeN += 1;
                             cfg.plunger.cal.tRelease = uint8_t(calRlsTimeSum / calRlsTimeN);
                         }
                     }
-                }
-                else if (v < vprv2)
-                {
-                    // We're still accelerating, and we haven't crossed the zero
-                    // point yet - stay in phase 1.  (Note that forward motion is
-                    // negative velocity, so accelerating means that the new 
-                    // velocity is more negative than the previous one, which
-                    // is to say numerically less than - that's why the test
-                    // for acceleration is the seemingly backwards 'v < vprv'.)
-
-                    // If we've been accelerating for at least 20ms, we're probably
-                    // really doing a release.  Jump back to the recent local
-                    // maximum where the release *really* started.  This is always
-                    // a bit before we started seeing sustained accleration, because
-                    // the plunger motion for the first few milliseconds is too slow
-                    // for our sensor precision to reliably detect acceleration.
-                    if (f1.pos != 0)
-                    {
-                        // we have a reset point - freeze there
-                        z = f1.pos;
-                    }
-                    else if (uint32_t(r.t - f1.t) >= 20000UL)
-                    {
-                        // it's been long enough - set a reset point.
-                        f1.pos = z = histLocalMax(r.t, 50000UL);
-                    }
+
+                    // Figure the "bounce" position as forward of the park
+                    // position by 1/6 of the starting retraction distance.
+                    // This simulates the momentum of the plunger compressing
+                    // the barrel spring on the rebound.  The barrel spring
+                    // can compress by about 1/6 of the maximum retraction 
+                    // distance, so we'll simply treat its compression as
+                    // proportional to the retraction.  (It might be more
+                    // realistic to use a slightly higher value here, maybe
+                    // 1/4 or 1/3 or the retraction distance, capping it at
+                    // a maximum of 1/6, because the real plunger probably 
+                    // compresses the barrel spring by 100% with less than 
+                    // 100% retraction.  But that won't affect the physics
+                    // meaningfully, just the animation, and the effect is
+                    // small in any case.)
+                    z = f0.pos = -f0.pos / 6;
+                    
+                    // reset the starting time for this phase
+                    f0.t = r.t;
                 }
                 else
                 {
-                    // We're not accelerating.  Cancel the firing event.
-                    firingMode(0);
-                    calState = 1;
+                    // check for motion since the start of the firing event
+                    const uint32_t dt = uint32_t(r.t - f0.t);
+                    const uint32_t dt2 = dt*dt;  // dt^2
+                    if (dt < 50000 
+                        && r.pos < f0.pos - int((f0.pos*acc2*uint64_t(dt2)) >> 48))
+                    {
+                        // It's moving fast enough to still be in a release
+                        // motion.  Continue reporting the start position, and
+                        // stay in the first release phase.
+                        z = f0.pos;
+                    }
+                    else
+                    {
+                        // It's not moving fast enough to be a release
+                        // motion.  Return to the default state.
+                        firingMode(0);
+                        calState = 1;
+                    }
                 }
                 break;
                 
             case 2:
-                // Phase 2 - start of synthetic firing event.  Report the fake
-                // bounce for 25ms.  VP polls the joystick about every 10ms, so 
-                // this should be enough time to guarantee that VP sees this
-                // report at least once.
-                if (uint32_t(r.t - f2.t) < 25000UL)
+                // Firing mode, holding at forward compression position.
+                // Hold here for 25ms.
+                if (uint32_t(r.t - f0.t) < 25000)
                 {
-                    // report the bounce position
-                    z = f2.pos;
+                    // stay here for now
+                    z = f0.pos;
                 }
                 else
                 {
-                    // it's been long enough - switch to phase 3, where we
-                    // report the park position until the real plunger comes
-                    // to rest
+                    // advance to the next phase, where we report the park
+                    // position until the plunger comes to rest
                     firingMode(3);
                     z = 0;
-                    
-                    // set the start of the "stability window" to the rest position
-                    f3s.t = r.t;
-                    f3s.pos = 0;
-                    
-                    // set the start of the "retraction window" to the actual position
-                    f3r = r;
+
+                    // remember when we started
+                    f0.t = r.t;
                 }
                 break;
                 
             case 3:
-                // Phase 3 - in synthetic firing event.  Report the park position
-                // until the plunger position stabilizes.  Left to its own devices, 
-                // the plunger will usualy bounce off the barrel spring several 
-                // times before coming to rest, so we'll see oscillating motion
-                // for a second or two.  In the simplest case, we can aimply wait
-                // for the plunger to stop moving for a short time.  However, the
-                // player might intervene by pulling the plunger back again, so
-                // watch for that motion as well.  If we're just bouncing freely,
-                // we'll see the direction change frequently.  If the player is
-                // moving the plunger manually, the direction will be constant
-                // for longer.
-                if (v >= 0)
+                // Firing event, holding at park position.  Stay here for
+                // a few moments so that the PC client can simulate the
+                // full release motion, then return to real readings.
+                if (uint32_t(r.t - f0.t) < 250000)
                 {
-                    // We're moving back (or standing still).  If this has been
-                    // going on for a while, the user must have taken control.
-                    if (uint32_t(r.t - f3r.t) > 65000UL)
-                    {
-                        // user has taken control - cancel firing mode
-                        firingMode(0);
-                        break;
-                    }
+                    // stay here a while longer
+                    z = 0;
                 }
                 else
                 {
-                    // forward motion - reset retraction window
-                    f3r.t = r.t;
-                }
-
-                // Check if we're close to the last starting point.  The joystick
-                // positive axis range (0..4096) covers the retraction distance of 
-                // about 2.5", so 1" is about 1638 joystick units, hence 1/16" is
-                // about 100 units.
-                if (abs(r.pos - f3s.pos) < 100)
-                {
-                    // It's at roughly the same position as the starting point.
-                    // Consider it stable if this has been true for 300ms.
-                    if (uint32_t(r.t - f3s.t) > 300000UL)
-                    {
-                        // we're done with the firing event
-                        firingMode(0);
-                    }
-                    else
-                    {
-                        // it's close to the last position but hasn't been
-                        // here long enough; stay in firing mode and continue
-                        // to report the park position
-                        z = 0;
-                    }
-                }
-                else
-                {
-                    // It's not close enough to the last starting point, so use
-                    // this as a new starting point, and stay in firing mode.
-                    f3s = r;
-                    z = 0;
+                    // it's been long enough - return to normal mode
+                    firingMode(0);
                 }
                 break;
             }
             
-            // save the velocity reading for next time
-            vprv2 = vprv;
-            vprv = v;
-            
             // Check for auto-zeroing, if enabled
             if ((cfg.plunger.autoZero.flags & PlungerAutoZeroEnabled) != 0)
             {
@@ -4837,10 +4887,8 @@
                 }
             }
             
-            // add the new reading to the history
-            hist[histIdx] = r;
-            if (++histIdx >= countof(hist))
-                histIdx = 0;
+            // this new reading becomes the previous reading for next time
+            prv = r;
         }
     }
     
@@ -4851,9 +4899,6 @@
         return z;
     }
         
-    // get the timestamp of the current joystick report (microseconds)
-    uint32_t getTimestamp() const { return nthHist(0).t; }
-
     // Set calibration mode on or off
     void setCalMode(bool f) 
     {
@@ -4942,6 +4987,12 @@
     bool isFiring() { return firing == 3; }
     
 private:
+    // current reported joystick reading
+    int z;
+    
+    // previous reading
+    PlungerReading prv;
+
     // Calibration state.  During calibration mode, we watch for release
     // events, to measure the time it takes to complete the release
     // motion; and we watch for the plunger to come to reset after a
@@ -4978,105 +5029,21 @@
         firing = m;
     }
     
-    // Find the most recent local maximum in the history data, up to
-    // the given time limit.
-    int histLocalMax(uint32_t tcur, uint32_t dt)
-    {
-        // start with the prior entry
-        int idx = (histIdx == 0 ? countof(hist) : histIdx) - 1;
-        int hi = hist[idx].pos;
-        
-        // scan backwards for a local maximum
-        for (int n = countof(hist) - 1 ; n > 0 ; idx = (idx == 0 ? countof(hist) : idx) - 1)
-        {
-            // if this isn't within the time window, stop
-            if (uint32_t(tcur - hist[idx].t) > dt)
-                break;
-                
-            // if this isn't above the current hith, stop
-            if (hist[idx].pos < hi)
-                break;
-                
-            // this is the new high
-            hi = hist[idx].pos;
-        }
-        
-        // return the local maximum
-        return hi;
-    }
-
-    // velocity at previous reading, and the one before that
-    int vprv, vprv2;
-    
-    // Circular buffer of recent readings.  We keep a short history
-    // of readings to analyze during firing events.  We can only identify
-    // a firing event once it's somewhat under way, so we need a little
-    // retrospective information to accurately determine after the fact
-    // exactly when it started.  We throttle our readings to no more
-    // than one every 1ms, so we have at least N*1ms of history in this
-    // array.
-    PlungerReading hist[32];
-    int histIdx;
-    
-    // get the nth history item (0=last, 1=2nd to last, etc)
-    inline const PlungerReading &nthHist(int n) const
-    {
-        // histIdx-1 is the last written; go from there
-        n = histIdx - 1 - n;
-        
-        // adjust for wrapping
-        if (n < 0)
-            n += countof(hist);
-            
-        // return the item
-        return hist[n];
-    }
-
     // Firing event state.
     //
-    //   0 - Default state.  We report the real instantaneous plunger 
-    //       position to the joystick interface.
-    //
-    //   1 - Moving forward
+    //   0 - Default state: not in firing event.  We report the true
+    //       instantaneous plunger position to the joystick interface.
     //
-    //   2 - Accelerating
+    //   1 - Moving forward at release speed
     //
-    //   3 - Firing.  We report the rest position for a minimum interval,
-    //       or until the real plunger comes to rest somewhere.
+    //   2 - Firing - reporting the bounce position
+    //
+    //   3 - Firing - reporting the park position
     //
     int firing;
     
-    // Position/timestamp at start of firing phase 1.  When we see a
-    // sustained forward acceleration, we freeze joystick reports at
-    // the recent local maximum, on the assumption that this was the
-    // start of the release.  If this is zero, it means that we're
-    // monitoring accelerating motion but haven't seen it for long
-    // enough yet to be confident that a release is in progress.
-    PlungerReading f1;
-    
-    // Position/timestamp at start of firing phase 2.  The position is
-    // the fake "bounce" position we report during this phase, and the
-    // timestamp tells us when the phase began so that we can end it
-    // after enough time elapses.
-    PlungerReading f2;
-    
-    // Position/timestamp of start of stability window during phase 3.
-    // We use this to determine when the plunger comes to rest.  We set
-    // this at the beginning of phase 3, and then reset it when the 
-    // plunger moves too far from the last position.
-    PlungerReading f3s;
-    
-    // Position/timestamp of start of retraction window during phase 3.
-    // We use this to determine if the user is drawing the plunger back.
-    // If we see retraction motion for more than about 65ms, we assume
-    // that the user has taken over, because we should see forward
-    // motion within this timeframe if the plunger is just bouncing
-    // freely.
-    PlungerReading f3r;
-    
-    // next Z value to report to the joystick interface (in joystick 
-    // distance units)
-    int z;
+    // Starting position for current firing mode phase
+    PlungerReading f0;
 };
 
 // plunger reader singleton
@@ -5574,10 +5541,9 @@
         // in a variable-dependent format.
         configVarSet(data);
         
-        // If updating the jitter window (variable 19), apply it immediately
-        // to the plunger sensor object
-        if (data[1] == 19)
-            plungerSensor->setJitterWindow(cfg.plunger.jitterWindow);
+        // notify the plunger, so that it can update relevant variables
+        // dynamically
+        plungerSensor->onConfigChange(data[1], cfg);
     }
     else if (data[0] == 67)
     {
@@ -5775,6 +5741,9 @@
     // set up the TLC5940 interface, if these chips are present
     init_tlc5940(cfg);
 
+    // initialize the TLC5916 interface, if these chips are present
+    init_tlc59116(cfg);
+    
     // set up 74HC595 interface, if these chips are present
     init_hc595(cfg);
     
@@ -5787,7 +5756,7 @@
     // start the TLC5940 refresh cycle clock
     if (tlc5940 != 0)
         tlc5940->start();
-
+        
     // Assume that nothing uses keyboard keys.  We'll check for keyboard
     // usage when initializing the various subsystems that can send keys
     // (buttons, IR).  If we find anything that does, we'll create the
@@ -5927,6 +5896,8 @@
         tlc5940->enable(true);
     if (hc595 != 0)
         hc595->enable(true);
+    if (tlc59116 != 0)
+        tlc59116->enable(true);
         
     // start the LedWiz flash cycle timer
     wizCycleTimer.start();
@@ -5985,6 +5956,10 @@
         // send TLC5940 data updates if applicable
         if (tlc5940 != 0)
             tlc5940->send();
+            
+        // send TLC59116 data updates
+        if (tlc59116 != 0)
+            tlc59116->send();
        
         // collect diagnostic statistics, checkpoint 1
         IF_DIAG(mainLoopIterCheckpt[1] += mainLoopTimer.read_us();)
@@ -6255,6 +6230,8 @@
                     // the power first comes on.
                     if (tlc5940 != 0)
                         tlc5940->enable(false);
+                    if (tlc59116 != 0)
+                        tlc59116->enable(false);
                     if (hc595 != 0)
                         hc595->enable(false);
                 }
@@ -6263,7 +6240,7 @@
         
         // if we have a reboot timer pending, check for completion
         if (saveConfigFollowupTimer.isRunning() 
-            && saveConfigFollowupTimer.read() > saveConfigFollowupTime)
+            && saveConfigFollowupTimer.read_us() > saveConfigFollowupTime*1000000UL)
         {
             // if a reboot is pending, execute it now
             if (saveConfigRebootPending)
@@ -6323,6 +6300,10 @@
                 // send TLC5940 data if necessary
                 if (tlc5940 != 0)
                     tlc5940->send();
+                    
+                // update TLC59116 outputs
+                if (tlc59116 != 0)
+                    tlc59116->send();
                 
                 // show a diagnostic flash every couple of seconds
                 if (diagTimer.read_us() > 2000000)
@@ -6364,10 +6345,9 @@
 
             // Enable peripheral chips and update them with current output data
             if (tlc5940 != 0)
-            {
                 tlc5940->enable(true);
-                tlc5940->update(true);
-            }
+            if (tlc59116 != 0)
+                tlc59116->enable(true);
             if (hc595 != 0)
             {
                 hc595->enable(true);