Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

Revision:
77:0b96f6867312
Parent:
76:7f5912b6340e
Child:
78:1e00b3fa11af
--- a/main.cpp	Fri Feb 03 20:50:02 2017 +0000
+++ b/main.cpp	Fri Mar 17 22:02:08 2017 +0000
@@ -52,9 +52,10 @@
 //    that's supported, along with physical mounting and wiring details, can be found
 //    in the Build Guide.
 //
-//    Note VP has built-in support for plunger devices like this one, but some VP
-//    tables can't use it without some additional scripting work.  The Build Guide has 
-//    advice on adjusting tables to add plunger support when necessary.
+//    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.
 //
 //    For best results, the plunger sensor should be calibrated.  The calibration
 //    is stored in non-volatile memory on board the KL25Z, so it's only necessary
@@ -75,12 +76,11 @@
 //    let it shoot forward too far.  We want to measure the range from the park
 //    position to the fully retracted position only.)
 //
-//  - Button input wiring.  24 of the KL25Z's GPIO ports are mapped as digital inputs
-//    for buttons and switches.  You can wire each input to a physical pinball-style
-//    button or switch, such as flipper buttons, Start buttons, coin chute switches,
-//    tilt bobs, and service buttons.  Each button can be configured to be reported
-//    to the PC as a joystick button or as a keyboard key (you can select which key
-//    is used for each button).
+//  - Button input wiring.  You can assign GPIO ports as inputs for physical
+//    pinball-style buttons, such as flipper buttons, a Start button, coin
+//    chute switches, tilt bobs, and service panel buttons.  You can configure
+//    each button input to report a keyboard key or joystick button press to
+//    the PC when the physical button is pushed.
 //
 //  - LedWiz emulation.  The KL25Z can pretend to be an LedWiz device.  This lets
 //    you connect feedback devices (lights, solenoids, motors) to GPIO ports on the 
@@ -140,9 +140,26 @@
 //    power to the cabinet comes on, with a configurable delay timer.  This feature
 //    is for TVs that don't turn themselves on automatically when first plugged in.
 //    To use this feature, you have to build some external circuitry to allow the
-//    software to sense the power supply status, and you have to run wires to your
-//    TV's on/off button, which requires opening the case on your TV.  The Build
-//    Guide has details on the necessary circuitry and connections to the TV.
+//    software to sense the power supply status.  The Build Guide has details 
+//    on the necessary circuitry.  You can use this to switch your TV on via a
+//    hardwired connection to the TV's "on" button, which requires taking the
+//    TV apart to gain access to its internal wiring, or optionally via the IR
+//    remote control transmitter feature below.
+//
+//  - Infrared (IR) remote control receiver and transmitter.  You can attach an
+//    IR LED and/or an IR sensor (we recommend the TSOP384xx series) to make the
+//    KL25Z capable of sending and/or receiving IR remote control signals.  This
+//    can be used with the TV ON feature above to turn your TV(s) on when the
+//    system power comes on by sending the "on" command to them via IR, as though
+//    you pressed the "on" button on the remote control.  The sensor lets the
+//    Pinscape software learn the IR codes from your existing remotes, in the
+//    same manner as a handheld universal remote control, and the IR LED lets
+//    it transmit learned codes.  The sensor can also be used to receive codes
+//    during normal operation and turn them into PC keystrokes; this lets you
+//    access extra commands on the PC without adding more buttons to your
+//    cabinet.  The IR LED can also be used to transmit other codes when you
+//    press selected cabinet buttons, allowing you to assign cabinet buttons
+//    to send IR commands to your cabinet TV or other devices.
 //
 //
 //
@@ -212,6 +229,9 @@
 #include "potSensor.h"
 #include "nullSensor.h"
 #include "TinyDigitalIn.h"
+#include "IRReceiver.h"
+#include "IRTransmitter.h"
+#include "NewPwm.h"
 
 
 #define DECL_EXTERNS
@@ -313,83 +333,73 @@
 // left over after counting the static writable data and reserving space
 // for a reasonable stack.  I haven't looked at the mbed malloc to see why 
 // they're so stingy, but it appears from empirical testing that we can 
-// create a static array up to about 9K before things get crashy.
-
-// Dynamic memory pool.  We'll reserve space for all dynamic 
-// allocations by creating a simple C array of bytes.  The size
-// of this array is the maximum number of bytes we can allocate
-// with malloc or operator 'new'.
+// create a static array up to about 8K before things get crashy.
+
+
+// halt with a diagnostic display if we run out of memory
+void HaltOutOfMem()
+{
+    printf("\r\nOut Of Memory\r\n");
+    // halt with the diagnostic display (by looping forever)
+    for (;;)
+    {
+        diagLED(1, 0, 0);
+        wait_us(200000);
+        diagLED(1, 0, 1);
+        wait_us(200000);
+    }
+}
+
+#if 0//$$$
+// Memory pool.  We allocate two blocks at fixed addresses: one for
+// the malloc heap, and one for the native stack.  
 //
-// The maximum safe size for this array is, in essence, the
-// amount of physical KL25Z RAM left over after accounting for
-// static data throughout the rest of the program, the run-time
-// stack, and any other space reserved for compiler or MCU
-// overhead.  Unfortunately, it's not straightforward to
-// determine this analytically.  The big complication is that
-// the minimum stack size isn't easily predictable, as the stack
-// grows according to what the program does.  In addition, the
-// mbed platform tools don't give us detailed data on the
-// compiler/linker memory map.  All we get is a generic total
-// RAM requirement, which doesn't necessarily account for all
-// overhead (e.g., gaps inserted to get proper alignment for
-// particular memory blocks).  
+// We allocate the stack block at the very top of memory.  This is what 
+// the mbed startup code does anyway, so we don't actually ever move the 
+// stack pointer into this area ourselves.  The point of this block is
+// to reserve space with the linker, so that it won't put any other static
+// data here in this region.
 //
-// A very rough estimate: the total RAM size reported by the 
-// linker is about 3.5K (currently - that can obviously change 
-// as the project evolves) out of 16K total.  Assuming about a 
-// 3K stack, that leaves in the ballpark of 10K.  Empirically,
-// that seems pretty close.  In testing, we start to see some
-// instability at 10K, while 9K seems safe.  To be conservative,
-// we'll reduce this to 8K.
+// The heap block goes just below the stack block.  This is a contiguous 
+// block of bytes from which we allocate blocks for malloc() and 'operator 
+// new' requests.
 //
-// Our measured total usage in the base configuration (22 GPIO
-// output ports, TSL1410R plunger sensor) is about 4000 bytes.
-// A pretty fully decked-out configuration (121 output ports,
-// with 8 TLC5940 chips and 3 74HC595 chips, plus the TSL1412R
-// sensor with the higher pixel count, and all expansion board
-// features enabled) comes to about 6700 bytes.  That leaves
-// us with about 1.5K free out of our 8K, so we still have a 
-// little more headroom for future expansion.
-//
-// For comparison, the standard mbed malloc() runs out of
-// memory at about 6K.  That's what led to this custom malloc:
-// we can just fit the base configuration into that 4K, but
-// it's not enough space for more complex setups.  There's
-// still a little room for squeezing out unnecessary space
-// from the mbed library code, but at this point I'd prefer
-// to treat that as a last resort, since it would mean having
-// to fork private copies of the libraries.
-static const size_t XMALLOC_POOL_SIZE = 8*1024;
-static char xmalloc_pool[XMALLOC_POOL_SIZE];
+// WARNING!  When adding static data, be sure to check the build statistics
+// to ensure that static data fits in the available RAM.  The linker doesn't
+// seem to make such a check on its own, so you might not see an error if
+// added data pushes us past the 16K limit.
+
+// KL25Z address of top of RAM (one byte past end of RAM)
+const uint32_t TOP_OF_RAM = 0x20003000UL;
+
+// malloc pool size
+const size_t XMALLOC_POOL_SIZE = 8*1024;
+
+// stack size
+const size_t XMALLOC_STACK_SIZE = 2*1024;
+
+// figure the fixed locations of the malloc pool and stack: the stack goes
+// at the very top of RAM, and the malloc pool goes just below the stack
+const uint32_t XMALLOC_STACK_BASE = TOP_OF_RAM - XMALLOC_STACK_SIZE;
+const uint32_t XMALLOC_POOL_BASE = XMALLOC_STACK_BASE - XMALLOC_POOL_SIZE;
+
+// allocate the pools - use __attribute__((at)) to give them fixed addresses
+static char xmalloc_stack[XMALLOC_STACK_SIZE] __attribute__((at(XMALLOC_STACK_BASE)));
+static char xmalloc_pool[XMALLOC_POOL_SIZE] __attribute__((at(XMALLOC_POOL_BASE)));
+
+// malloc pool free pointer and space remaining
 static char *xmalloc_nxt = xmalloc_pool;
 static size_t xmalloc_rem = XMALLOC_POOL_SIZE;
     
+// allocate from our pool
 void *xmalloc(size_t siz)
 {
     // align to a 4-byte increment
     siz = (siz + 3) & ~3;
     
-    // If insufficient memory is available, halt and show a fast red/purple 
-    // diagnostic flash.  We don't want to return, since we assume throughout
-    // the program that all memory allocations must succeed.  Note that this
-    // is generally considered bad programming practice in applications on
-    // "real" computers, but for the purposes of this microcontroller app,
-    // there's no point in checking for failed allocations individually
-    // because there's no way to recover from them.  It's better in this 
-    // context to handle failed allocations as fatal errors centrally.  We
-    // can't recover from these automatically, so we have to resort to user
-    // intervention, which we signal with the diagnostic LED flashes.
+    // if we're out of memory, halt with a diagnostic display
     if (siz > xmalloc_rem)
-    {
-        // halt with the diagnostic display (by looping forever)
-        for (;;)
-        {
-            diagLED(1, 0, 0);
-            wait_us(200000);
-            diagLED(1, 0, 1);
-            wait_us(200000);
-        }
-    }
+        HaltOutOfMem();
 
     // get the next free location from the pool to return   
     char *ret = xmalloc_nxt;
@@ -401,8 +411,89 @@
     // return the allocated block
     return ret;
 };
-
-// our malloc() replacement
+#elif 1//$$$
+// For our custom malloc, we take advantage of the known layout of the
+// mbed library memory management.  The mbed library puts all of the
+// static read/write data at the low end of RAM; this includes the
+// initialized statics and the "ZI" (zero-initialized) statics.  The
+// malloc heap starts just after the last static, growing upwards as
+// memory is allocated.  The stack starts at the top of RAM and grows
+// downwards.  
+//
+// To figure out where the free memory starts, we simply call the system
+// malloc() to make a dummy allocation the first time we're called, and 
+// use the address it returns as the start of our free memory pool.  The
+// first malloc() call presumably returns the lowest byte of the pool in
+// the compiler RTL's way of thinking, and from what we know about the
+// mbed heap layout, we know everything above this point should be free,
+// at least until we reach the lowest address used by the stack.
+//
+// The ultimate size of the stack is of course dynamic and unpredictable.
+// In testing, it appears that we currently need a little over 1K.  To be
+// conservative, we'll reserve 2K for the stack, by taking it out of the
+// space at top of memory we consider fair game for malloc.
+//
+// Note that we could do this a little more low-level-ly if we wanted.
+// The ARM linker provides a pre-defined extern char[] variable named 
+// Image$$RW_IRAM1$$ZI$$Limit, which is always placed just after the
+// last static data variable.  In principle, this tells us the start
+// of the available malloc pool.  However, in testing, it doesn't seem
+// safe to use this as the start of our malloc pool.  I'm not sure why,
+// but probably something in the startup code (either in the C RTL or 
+// the mbed library) is allocating from the pool before we get control. 
+// So we won't use that approach.  Besides, that would tie us even more
+// closely to the ARM compiler.  With our malloc() probe approach, we're
+// at least portable to any compiler that uses the same basic memory
+// layout, with the heap above the statics and the stack at top of 
+// memory; this isn't universal, but it's very typical.
+
+static char *xmalloc_nxt = 0;
+size_t xmalloc_rem = 0;
+void *xmalloc(size_t siz)
+{
+    if (xmalloc_nxt == 0)
+    {
+        xmalloc_nxt = (char *)malloc(4);
+        xmalloc_rem = 0x20003000UL - 2*1024 - uint32_t(xmalloc_nxt);
+    }
+    
+    siz = (siz + 3) & ~3;
+    if (siz > xmalloc_rem)
+        HaltOutOfMem();
+        
+    char *ret = xmalloc_nxt;
+    xmalloc_nxt += siz;
+    xmalloc_rem -= siz;
+    
+    return ret;
+}
+#else //$$$
+extern char Image$$RW_IRAM1$$ZI$$Limit[]; // linker marker for top of ZI region
+static char *xmalloc_nxt = Image$$RW_IRAM1$$ZI$$Limit;
+const uint32_t xmallocMinStack = 2*1024;
+char *const TopOfRAM = (char *)0x20003000UL;
+uint16_t xmalloc_rem = uint16_t(TopOfRAM - Image$$RW_IRAM1$$ZI$$Limit - xmallocMinStack);
+void *xmalloc(size_t siz)
+{
+    // align to a 4-byte increment
+    siz = (siz + 3) & ~3;
+    
+    // check to ensure we're leaving enough stack free
+    if (xmalloc_nxt + siz > TopOfRAM - xmallocMinStack)
+        HaltOutOfMem();
+        
+    // get the next free location from the pool to return
+    char *ret = xmalloc_nxt;
+    
+    // advance past the allocated memory
+    xmalloc_nxt += siz;
+    xmalloc_rem -= siz;
+    
+    // return the allocated block
+    printf("malloc(%d) -> %lx\r\n", siz, ret);
+    return ret;
+}
+#endif//$$$
 
 // Overload operator new to call our custom malloc.  This ensures that
 // all 'new' allocations throughout the program (including library code)
@@ -427,7 +518,8 @@
 // ---------------------------------------------------------------------------
 // utilities
 
-// floating point square of a number
+// int/float point square of a number
+inline int square(int x) { return x*x; }
 inline float square(float x) { return x*x; }
 
 // floating point rounding
@@ -439,10 +531,10 @@
 // Extended verison of Timer class.  This adds the ability to interrogate
 // the running state.
 //
-class Timer2: public Timer
+class ExtTimer: public Timer
 {
 public:
-    Timer2() : running(false) { }
+    ExtTimer() : running(false) { }
 
     void start() { running = true; Timer::start(); }
     void stop()  { running = false; Timer::stop(); }
@@ -455,13 +547,6 @@
 
 
 // --------------------------------------------------------------------------
-//
-// Reboot timer.  When we have a deferred reboot operation pending, we
-// set the target time and start the timer.
-Timer2 rebootTimer;
-long rebootTime_us;
-
-// --------------------------------------------------------------------------
 // 
 // USB product version number
 //
@@ -564,7 +649,8 @@
 DigitalOut *ledR, *ledG, *ledB;
 
 // Power on timer state for diagnostics.  We flash the blue LED when
-// nothing else is going on.  State 0-1 = off, 2-3 = on
+// nothing else is going on.  State 0-1 = off, 2-3 = on blue.  Also
+// show red when transmitting an LED signal, indicated by state 4.
 uint8_t powerTimerDiagState = 0;
 
 // Show the indicated pattern on the diagnostic LEDs.  0 is off, 1 is
@@ -578,7 +664,10 @@
     // if turning everything off, use the power timer state instead, 
     // applying it to the blue LED
     if (diagLEDState == 0)
-        b = (powerTimerDiagState >= 2);
+    {
+        b = (powerTimerDiagState == 2 || powerTimerDiagState == 3);
+        r = (powerTimerDiagState == 4);
+    }
         
     // set the new state
     if (ledR != 0 && r != -1) ledR->write(!r);
@@ -592,7 +681,7 @@
     diagLED(
         diagLEDState & 0x01,
         (diagLEDState >> 1) & 0x01,
-        (diagLEDState >> 1) & 0x02);
+        (diagLEDState >> 2) & 0x01);
 }
 
 // check an output port assignment to see if it conflicts with
@@ -868,8 +957,10 @@
     LwOut *out;
 };
 
-// global night mode flag
-static bool nightMode = false;
+// Global night mode flag.  To minimize overhead when reporting
+// the status, we set this to the status report flag bit for
+// night mode, 0x02, when engaged.
+static uint8_t nightMode = 0x00;
 
 // Noisy output.  This is a filter object that we layer on top of
 // a physical pin output.  This filter disables the port when night
@@ -1122,110 +1213,55 @@
     0.925022f, 0.935504f, 0.946062f, 0.956696f, 0.967407f, 0.978194f, 0.989058f, 1.000000f
 };
 
-// MyPwmOut - a slight customization of the base mbed PwmOut class.  The 
-// mbed version of PwmOut.write() resets the PWM cycle counter on every 
-// update.  That's problematic, because the counter reset interrupts the
-// cycle in progress, causing a momentary drop in brightness that's visible
-// to the eye if the output is connected to an LED or other light source.
-// This is especially noticeable when making gradual changes consisting of
-// many updates in a short time, such as a slow fade, because the light 
-// visibly flickers on every step of the transition.  This customized 
-// version removes the cycle reset, which makes for glitch-free updates 
-// and nice smooth fades.
+// Polled-update PWM output list
 //
-// Initially, I thought the counter reset in the mbed code was simply a
-// bug.  According to the KL25Z hardware reference, you update the duty
-// cycle by writing to the "compare values" (CvN) register.  There's no
-// hint that you should reset the cycle counter, and indeed, the hardware
-// goes out of its way to allow updates mid-cycle (as we'll see shortly).
-// They went to lengths specifically so that you *don't* have to reset
-// that counter.  And there's no comment in the mbed code explaining the
-// cycle reset, so it looked to me like something that must have been
-// added by someone who didn't read the manual carefully enough and didn't
-// test the result thoroughly enough to find the glitch it causes.
+// This is a workaround for a KL25Z hardware bug/limitation.  The bug (more
+// about this below) is that we can't write to a PWM output "value" register
+// more than once per PWM cycle; if we do, outputs after the first are lost.
+// The value register controls the duty cycle, so it's what you have to write
+// if you want to update the brightness of an output.
 //
-// After some experimentation, though, I've come to think the code was
-// added intentionally, as a workaround for a rather nasty KL25Z hardware
-// bug.   Whoever wrote the code didn't add any comments explaning why it's
-// there, so we can't know for sure, but it does happen to work around the 
-// bug, so it's a good bet the original programmer found the same hardware
-// problem and came up with the counter reset as an imperfect solution.
+// Our solution is to simply repeat all PWM updates periodically.  If a write
+// is lost on one cycle, it'll eventually be applied on a subseuqent periodic
+// update.  For low overhead, we do these repeat updates periodically during
+// the main loop.
 //
-// We'll get to the KL25Z hardware bug shortly, but first we need to look at
-// how the hardware is *supposed* to work.  The KL25Z is *supposed* to make
-// it super easy for software to do glitch-free updates of the duty cycle of 
-// a PWM channel.  With PWM hardware in general, you have to be careful to
-// update the duty cycle counter between grayscale cycles, beacuse otherwise
-// you might interrupt the cycle in progress and cause a brightness glitch.  
-// The KL25Z TPM simplifies this with a "staging" register for the duty
-// cycle counter.  At the end of each cycle, the TPM moves the value from
-// the staging register into its internal register that actually controls 
-// the duty cycle.  The idea is that the software can write a new value to
-// the staging register at any time, and the hardware will take care of
-// synchronizing the actual internal update with the grayscale cycle.  In
-// principle, this frees the software of any special timing considerations
-// for PWM updates.  
+// The mbed library has its own solution to this bug, but it creates a 
+// separate problem of its own.  The mbed solution is to write the value
+// register immediately, and then also reset the "count" register in the 
+// TPM unit containing the output.  The count reset truncates the current
+// PWM cycle, which avoids the hardware problem with more than one write per
+// cycle.  The problem is that the truncated cycle causes visible flicker if
+// the output is connected to an LED.  This is particularly noticeable during
+// fades, when we're updating the value register repeatedly and rapidly: an
+// attempt to fade from fully on to fully off causes rapid fluttering and 
+// flashing rather than a smooth brightness fade.
 //
-// Now for the bug.  The staging register works as advertised, except for
-// one little detail: it seems to be implemented as a one-element queue
-// that won't accept a new write until the existing value has been read.
-// The read only happens at the start of the new cycle.  So the effect is
-// that we can only write one update per cycle.  Any writes after the first
-// are simply dropped, lost forever.  That causes even worse problems than
-// the original glitch.  For example, if we're doing a fade-out, the last
-// couple of updates in the fade might get lost, leaving the output slightly
-// on at the end, when it's supposed to be completely off.
-//
-// The mbed workaround of resetting the cycle counter fixes the lost-update
-// problem, but it causes the constant glitching during fades.  So we need
-// a third way that works around the hardware problem without causing 
-// update glitches.
+// The hardware bug is a case of good intentions gone bad.  The hardware is
+// *supposed* to make it easy for software to avoid glitching during PWM
+// updates, by providing a staging register in front of the real value
+// register.  The software actually writes to the staging register, which
+// holds updates until the end of the cycle, at which point the hardware
+// automatically moves the value from the staging register into the real
+// register.  This ensures that the real register is always updated exactly
+// at a cycle boundary, which in turn ensures that there's no flicker when
+// values are updated.  A great design - except that it doesn't quite work.
+// The problem is that the staging register actually seems to be implemented
+// as a one-element FIFO in "stop when full" mode.  That is, when you write
+// the FIFO, it becomes full.  When the cycle ends and the hardware reads it
+// to move the staged value into the real register, the FIFO becomes empty.
+// But if you try to write the FIFO twice before the hardware reads it and
+// empties it, the second write fails, leaving the first value in the queue.
+// There doesn't seem to be any way to clear the FIFO from software, so you
+// just have to wait for the cycle to end before writing another update.
+// That more or less defeats the purpose of the staging register, whose whole
+// point is to free software from worrying about timing considerations with
+// updates.  It frees us of the need to align our timing on cycle boundaries,
+// but it leaves us with the need to limit writes to once per cycle.
 //
-// Here's my solution: we basically implement our own staging register,
-// using the same principle as the hardware staging register, but hopefully
-// with an implementation that actually works!  First, when we update a PWM 
-// output, we won't actually write the value to the hardware register.
-// Instead, we'll just stash it internally, effectively in our own staging
-// register (but actually just a member variable of this object).  Then
-// we'll periodically transfer these staged updates to the actual hardware 
-// registers, being careful to do this no more than once per PWM cycle.
-// One way to do this would be to use an interrupt handler that fires at
-// the end of the PWM cycle, but that would be fairly complex because we
-// have many (up to 10) PWM channels.  Instead, we'll just use polling:
-// we'll call a routine periodically in our main loop, and we'll transfer
-// updates for all of the channels that have been updated since the last
-// pass.  We can get away with this simple polling approach because the
-// hardware design *partially* works: it does manage to free us from the
-// need to synchronize updates with the exact end of a PWM cycle.  As long
-// as we do no more than one write per cycle, we're golden.  That's easy
-// to accomplish, too: all we need to do is make sure that our polling
-// interval is slightly longer than the PWM period.  That ensures that
-// we can never have two updates during one PWM cycle.  It does mean that
-// we might have zero updates on some cycles, causing a one-cycle delay
-// before an update is actually put into effect, but that shouldn't ever
-// be noticeable since the cycles are so short.  Specifically, we'll use
-// the mbed default 20ms PWM period, and we'll do our update polling 
-// every 25ms.
-class LessGlitchyPwmOut: public PwmOut
-{
-public:
-    LessGlitchyPwmOut(PinName pin) : PwmOut(pin) { }
-    
-    void write(float value)
-    {
-        // Update the counter without resetting the counter.
-        //
-        // NB: this causes problems if there are multiple writes in one
-        // PWM cycle: the first write will be applied and later writes 
-        // during the same cycle will be lost.  Callers must take care
-        // to limit writes to one per cycle.
-        *_pwm.CnV = uint32_t((*_pwm.MOD + 1) * value);
-    }
-};
-
-
-// Collection of PwmOut objects to update on each polling cycle.  The
-// KL25Z has 10 physical PWM channels, so we need at most 10 polled outputs.
+// So here we have our list of PWM outputs that need to be polled for updates.
+// The KL25Z hardware only has 10 PWM channels, so we only need a fixed set
+// of polled items.
 static int numPolledPwm;
 static class LwPwmOut *polledPwm[10];
 
@@ -1235,44 +1271,38 @@
 public:
     LwPwmOut(PinName pin, uint8_t initVal) : p(pin)
     {
-         // set the cycle time to 20ms
-         p.period_ms(20);
-         
-         // add myself to the list of polled outputs for periodic updates
-         if (numPolledPwm < countof(polledPwm))
+        // add myself to the list of polled outputs for periodic updates
+        if (numPolledPwm < countof(polledPwm))
             polledPwm[numPolledPwm++] = this;
-         
-         // set the initial value, and an explicitly different previous value
-         prv = ~initVal;
-         set(initVal);
+
+        // set the initial value
+        set(initVal);
     }
 
     virtual void set(uint8_t val) 
     {
-        // on set, just save the value for a later 'commit' 
+        // save the new value
         this->val = val;
+        
+        // commit it to the hardware
+        commit();
     }
 
     // handle periodic update polling
     void poll()
     {
-        // if the value has changed, commit it
-        if (val != prv)
-        {
-            prv = val;
-            commit(val);
-        }
+        commit();
     }
 
 protected:
-    virtual void commit(uint8_t v)
+    virtual void commit()
     {
         // write the current value to the PWM controller if it's changed
-        p.write(dof_to_pwm[v]);
+        p.glitchFreeWrite(dof_to_pwm[val]);
     }
     
-    LessGlitchyPwmOut p;
-    uint8_t val, prv;
+    NewPwmOut p;
+    uint8_t val;
 };
 
 // Gamma corrected PWM GPIO output.  This works exactly like the regular
@@ -1287,10 +1317,10 @@
     }
     
 protected:
-    virtual void commit(uint8_t v)
+    virtual void commit()
     {
         // write the current value to the PWM controller if it's changed
-        p.write(dof_to_gamma_pwm[v]);
+        p.glitchFreeWrite(dof_to_gamma_pwm[val]);
     }
 };
 
@@ -1896,6 +1926,454 @@
         hc595->update();
 }
 
+// ---------------------------------------------------------------------------
+//
+// IR Remote Control transmitter & receiver
+//
+
+// receiver
+IRReceiver *ir_rx;
+
+// transmitter
+IRTransmitter *ir_tx;
+
+// Mapping from IR commands slots in the configuration to "virtual button"
+// numbers on the IRTransmitter's "virtual remote".  To minimize RAM usage, 
+// we only create virtual buttons on the transmitter object for code slots 
+// that are configured for transmission, which includes slots used for TV
+// ON commands and slots that can be triggered by button presses.  This
+// means that virtual button numbers won't necessarily match the config
+// slot numbers.  This table provides the mapping:
+// IRConfigSlotToVirtualButton[n] = ir_tx virtual button number for 
+// configuration slot n
+uint8_t IRConfigSlotToVirtualButton[MAX_IR_CODES];
+uint8_t IRAdHocSlot;
+
+// IR mode timer.  In normal mode, this is the time since the last
+// command received; we use this to handle commands with timed effects,
+// such as sending a key to the PC.  In learning mode, this is the time
+// since we activated learning mode, which we use to automatically end
+// learning mode if a decodable command isn't received within a reasonable
+// amount of time.
+Timer IRTimer;
+
+// IR Learning Mode.  The PC enters learning mode via special function 65 12.
+// The states are:
+//
+//   0 -> normal operation (not in learning mode)
+//   1 -> learning mode; reading raw codes, no command read yet
+//   2 -> learning mode; command received, awaiting auto-repeat
+//   3 -> learning mode; done, command and repeat mode decoded
+//
+// When we enter learning mode, we reset IRTimer to keep track of how long
+// we've been in the mode.  This allows the mode to time out if no code is
+// received within a reasonable time.
+uint8_t IRLearningMode = 0;
+
+// Learning mode command received.  This stores the first decoded command
+// when in learning mode.  For some protocols, we can't just report the
+// first command we receive, because we need to wait for an auto-repeat to
+// determine what format the remote uses for repeats.  This stores the first
+// command while we await a repeat.  This is necessary for protocols that 
+// have "dittos", since some remotes for such protocols use the dittos and 
+// some don't; the only way to find out is to read a repeat code and see if 
+// it's a ditto or just a repeat of the full code.
+IRCommand learnedIRCode;
+
+// IR comkmand received, as a config slot index, 1..MAX_IR_CODES.
+// When we receive a command that matches one of our programmed commands, 
+// we note the slot here.  We also reset the IR timer so that we know how 
+// long it's been since the command came in.  This lets us handle commands 
+// with timed effects, such as PC key input.  Note that this is a 1-based 
+// index; 0 represents no command.
+uint8_t IRCommandIn = 0;
+
+// "Toggle bit" of last command.  Some IR protocols have a toggle bit
+// that distinguishes an auto-repeating key from a key being pressed
+// several times in a row.  This records the toggle bit of the last
+// command we received.
+uint8_t lastIRToggle = 0;
+
+// Are we in a gap between successive key presses?  When we detect that a 
+// key is being pressed multiple times rather than auto-repeated (which we 
+// can detect via a toggle bit in some protocols), we'll briefly stop sending 
+// the associated key to the PC, so that the PC likewise recognizes the 
+// distinct key press.  
+uint8_t IRKeyGap = false;
+
+// initialize
+void init_IR(Config &cfg, bool &kbKeys)
+{
+    PinName pin;
+    
+    // start the IR timer
+    IRTimer.start();
+    
+    // if there's a transmitter, set it up
+    if ((pin = wirePinName(cfg.IR.emitter)) != NC)
+    {
+        // no virtual buttons yet
+        int nVirtualButtons = 0;
+        memset(IRConfigSlotToVirtualButton, 0xFF, sizeof(IRConfigSlotToVirtualButton));
+        
+        // assign virtual buttons slots for TV ON codes
+        for (int i = 0 ; i < MAX_IR_CODES ; ++i)
+        {
+            if ((cfg.IRCommand[i].flags & IRFlagTVON) != 0)
+                IRConfigSlotToVirtualButton[i] = nVirtualButtons++;
+        }
+            
+        // assign virtual buttons for codes that can be triggered by 
+        // real button inputs
+        for (int i = 0 ; i < MAX_BUTTONS ; ++i)
+        {
+            // get the button
+            ButtonCfg &b = cfg.button[i];
+            
+            // check the unshifted button
+            int c = b.IRCommand - 1;
+            if (c >= 0 && c < MAX_IR_CODES 
+                && IRConfigSlotToVirtualButton[c] == 0xFF)
+                IRConfigSlotToVirtualButton[c] = nVirtualButtons++;
+                
+            // check the shifted button
+            c = b.IRCommand2 - 1;
+            if (c >= 0 && c < MAX_IR_CODES 
+                && IRConfigSlotToVirtualButton[c] == 0xFF)
+                IRConfigSlotToVirtualButton[c] = nVirtualButtons++;
+        }
+        
+        // allocate an additional virtual button for transmitting ad hoc
+        // codes, such as for the "send code" USB API function
+        IRAdHocSlot = nVirtualButtons++;
+            
+        // create the transmitter
+        ir_tx = new IRTransmitter(pin, nVirtualButtons);
+        
+        // program the commands into the virtual button slots
+        for (int i = 0 ; i < MAX_IR_CODES ; ++i)
+        {
+            // if this slot is assigned to a virtual button, program it
+            int vb = IRConfigSlotToVirtualButton[i];
+            if (vb != 0xFF)
+            {
+                IRCommandCfg &cb = cfg.IRCommand[i];
+                uint64_t code = cb.code.lo | (uint64_t(cb.code.hi) << 32);
+                bool dittos = (cb.flags & IRFlagDittos) != 0;
+                ir_tx->programButton(vb, cb.protocol, dittos, code);
+            }
+        }
+    }
+
+    // if there's a receiver, set it up
+    if ((pin = wirePinName(cfg.IR.sensor)) != NC)
+    {
+        // create the receiver
+        ir_rx = new IRReceiver(pin, 32);
+        
+        // connect the transmitter (if any) to the receiver, so that
+        // the receiver can suppress reception of our own transmissions
+        ir_rx->setTransmitter(ir_tx);
+        
+        // enable it
+        ir_rx->enable();
+        
+        // Check the IR command slots to see if any slots are configured
+        // to send a keyboard key on receiving an IR command.  If any are,
+        // tell the caller that we need a USB keyboard interface.
+        for (int i = 0 ; i < MAX_IR_CODES ; ++i)
+        {
+            IRCommandCfg &cb = cfg.IRCommand[i];
+            if (cb.protocol != 0
+                && (cb.keytype == BtnTypeKey || cb.keytype == BtnTypeMedia))
+            {
+                kbKeys = true;
+                break;
+            }
+        }
+    }
+}
+
+// Press or release a button with an assigned IR function.  'cmd'
+// is the command slot number (1..MAX_IR_CODES) assigned to the button.
+void IR_buttonChange(uint8_t cmd, bool pressed)
+{
+    // only proceed if there's an IR transmitter attached
+    if (ir_tx != 0)
+    {
+        // adjust the command slot to a zero-based index
+        int slot = cmd - 1;
+        
+        // press or release the virtual button
+        ir_tx->pushButton(IRConfigSlotToVirtualButton[slot], pressed);
+    }
+}
+
+// Process IR input
+void process_IR(Config &cfg, USBJoystick &js)
+{
+    // if there's no IR receiver attached, there's nothing to do
+    if (ir_rx == 0)
+        return;
+        
+    // Time out any received command
+    if (IRCommandIn != 0)
+    {
+        // Time out inter-key gap mode after 30ms; time out all 
+        // commands after 100ms.
+        uint32_t t = IRTimer.read_us();
+        if (t > 100000)
+            IRCommandIn = 0;
+        else if (t > 30000)
+            IRKeyGap = false;
+    }
+
+    // Check if we're in learning mode
+    if (IRLearningMode != 0)
+    {
+        // Learning mode.  Read raw inputs from the IR sensor and 
+        // forward them to the PC via USB reports, up to the report
+        // limit.
+        const int nmax = USBJoystick::maxRawIR;
+        uint16_t raw[nmax];
+        int n;
+        for (n = 0 ; n < nmax && ir_rx->processOne(raw[n]) ; ++n) ;
+        
+        // if we read any raw samples, report them
+        if (n != 0)
+            js.reportRawIR(n, raw);
+            
+        // check for a command
+        IRCommand c;
+        if (ir_rx->readCommand(c))
+        {
+            // check the current learning state
+            switch (IRLearningMode)
+            {
+            case 1:
+                // Initial state, waiting for the first decoded command.
+                // This is it.
+                learnedIRCode = c;
+                
+                // Check if we need additional information.  If the
+                // protocol supports dittos, we have to wait for a repeat
+                // to see if the remote actually uses the dittos, since
+                // some implementations of such protocols use the dittos
+                // while others just send repeated full codes.  Otherwise,
+                // all we need is the initial code, so we're done.
+                IRLearningMode = (c.hasDittos ? 2 : 3);
+                break;
+                
+            case 2:
+                // Code received, awaiting auto-repeat information.  If
+                // the protocol has dittos, check to see if we got a ditto:
+                //
+                // - If we received a ditto in the same protocol as the
+                //   prior command, the remote uses dittos.
+                //
+                // - If we received a repeat of the prior command (not a
+                //   ditto, but a repeat of the full code), the remote
+                //   doesn't use dittos even though the protocol supports
+                //   them.
+                //
+                // - Otherwise, it's not an auto-repeat at all, so we
+                //   can't decide one way or the other on dittos: start
+                //   over.
+                if (c.proId == learnedIRCode.proId
+                    && c.hasDittos
+                    && c.ditto)
+                {
+                    // success - the remote uses dittos
+                    IRLearningMode = 3;
+                }
+                else if (c.proId == learnedIRCode.proId
+                    && c.hasDittos
+                    && !c.ditto
+                    && c.code == learnedIRCode.code)
+                {
+                    // success - it's a repeat of the last code, so
+                    // the remote doesn't use dittos even though the
+                    // protocol supports them
+                    learnedIRCode.hasDittos = false;
+                    IRLearningMode = 3;
+                }
+                else
+                {
+                    // It's not a ditto and not a full repeat of the
+                    // last code, so it's either a new key, or some kind
+                    // of multi-code key encoding that we don't recognize.
+                    // We can't use this code, so start over.
+                    IRLearningMode = 1;
+                }
+                break;
+            }
+            
+            // If we ended in state 3, we've successfully decoded
+            // the transmission.  Report the decoded data and terminate
+            // learning mode.
+            if (IRLearningMode == 3)
+            {
+                // figure the flags: 
+                //   0x02 -> dittos
+                uint8_t flags = 0;
+                if (learnedIRCode.hasDittos)
+                    flags |= 0x02;
+                    
+                // report the code
+                js.reportIRCode(learnedIRCode.proId, flags, learnedIRCode.code);
+                    
+                // exit learning mode
+                IRLearningMode = 0;
+            }
+        }
+        
+        // time out of IR learning mode if it's been too long
+        if (IRLearningMode != 0 && IRTimer.read_us() > 10000000L)
+        {
+            // report the termination by sending a raw IR report with
+            // zero data elements
+            js.reportRawIR(0, 0);
+            
+            
+            // cancel learning mode
+            IRLearningMode = 0;
+        }
+    }
+    else
+    {
+        // Not in learning mode.  We don't care about the raw signals;
+        // just run them through the protocol decoders.
+        ir_rx->process();
+        
+        // Check for decoded commands.  Keep going until all commands
+        // have been read.
+        IRCommand c;
+        while (ir_rx->readCommand(c))
+        {
+            // We received a decoded command.  Determine if it's a repeat,
+            // and if so, try to determine whether it's an auto-repeat (due
+            // to the remote key being held down) or a distinct new press 
+            // on the same key as last time.  The distinction is significant
+            // because it affects the auto-repeat behavior of the PC key
+            // input.  An auto-repeat represents a key being held down on
+            // the remote, which we want to translate to a (virtual) key 
+            // being held down on the PC keyboard; a distinct key press on
+            // the remote translates to a distinct key press on the PC.
+            //
+            // It can only be a repeat if there's a prior command that
+            // hasn't timed out yet, so start by checking for a previous
+            // command.
+            bool repeat = false, autoRepeat = false;
+            if (IRCommandIn != 0)
+            {
+                // We have a command in progress.  Check to see if the
+                // new command is a repeat of the previous command.  Check
+                // first to see if it's a "ditto", which explicitly represents
+                // an auto-repeat of the last command.
+                IRCommandCfg &cmdcfg = cfg.IRCommand[IRCommandIn - 1];
+                if (c.ditto)
+                {
+                    // We received a ditto.  Dittos are always auto-
+                    // repeats, so it's an auto-repeat as long as the
+                    // ditto is in the same protocol as the last command.
+                    // If the ditto is in a new protocol, the ditto can't
+                    // be for the last command we saw, because a ditto
+                    // never changes protocols from its antecedent.  In
+                    // such a case, we must have missed the antecedent
+                    // command and thus don't know what's being repeated.
+                    repeat = autoRepeat = (c.proId == cmdcfg.protocol);
+                }
+                else
+                {
+                    // It's not a ditto.  The new command is a repeat if
+                    // it matches the protocol and command code of the 
+                    // prior command.
+                    repeat = (c.proId == cmdcfg.protocol 
+                              && uint32_t(c.code) == cmdcfg.code.lo
+                              && uint32_t(c.code >> 32) == cmdcfg.code.hi);
+                              
+                    // If the command is a repeat, try to determine whether
+                    // it's an auto-repeat or a new press on the same key.
+                    // If the protocol uses dittos, it's definitely a new
+                    // key press, because an auto-repeat would have used a
+                    // ditto.  For a protocol that doesn't use dittos, both
+                    // an auto-repeat and a new key press just send the key
+                    // code again, so we can't tell the difference based on
+                    // that alone.  But if the protocol has a toggle bit, we
+                    // can tell by the toggle bit value: a new key press has
+                    // the opposite toggle value as the last key press, while 
+                    // an auto-repeat has the same toggle.  Note that if the
+                    // protocol doesn't use toggle bits, the toggle value
+                    // will always be the same, so we'll simply always treat
+                    // any repeat as an auto-repeat.  Many protocols simply
+                    // provide no way to distinguish the two, so in such
+                    // cases it's consistent with the native implementations
+                    // to treat any repeat as an auto-repeat.
+                    autoRepeat = 
+                        repeat 
+                        && !(cmdcfg.flags & IRFlagDittos)
+                        && c.toggle == lastIRToggle;
+                }
+            }
+            
+            // Check to see if it's a repeat of any kind
+            if (repeat)
+            {
+                // It's a repeat.  If it's not an auto-repeat, it's a
+                // new distinct key press, so we need to send the PC a
+                // momentary gap where we're not sending the same key,
+                // so that the PC also recognizes this as a distinct
+                // key press event.
+                if (!autoRepeat)
+                    IRKeyGap = true;
+                    
+                // restart the key-up timer
+                IRTimer.reset();
+            }
+            else if (c.ditto)
+            {
+                // It's a ditto, but not a repeat of the last command.
+                // But a ditto doesn't contain any information of its own
+                // on the command being repeated, so given that it's not
+                // our last command, we can't infer what command the ditto
+                // is for and thus can't make sense of it.  We have to
+                // simply ignore it and wait for the sender to start with
+                // a full command for a new key press.
+                IRCommandIn = 0;
+            }
+            else
+            {
+                // It's not a repeat, so the last command is no longer
+                // in effect (regardless of whether we find a match for
+                // the new command).
+                IRCommandIn = 0;
+                
+                // Check to see if we recognize the new command, by
+                // searching for a match in our learned code list.
+                for (int i = 0 ; i < MAX_IR_CODES ; ++i)
+                {
+                    // if the protocol and command code from the code
+                    // list both match the input, it's a match
+                    IRCommandCfg &cmdcfg = cfg.IRCommand[i];
+                    if (cmdcfg.protocol == c.proId 
+                        && cmdcfg.code.lo == uint32_t(c.code)
+                        && cmdcfg.code.hi == uint32_t(c.code >> 32))
+                    {
+                        // Found it!  Make this the last command, and 
+                        // remember the starting time.
+                        IRCommandIn = i + 1;
+                        lastIRToggle = c.toggle;
+                        IRTimer.reset();
+                        
+                        // no need to keep searching
+                        break;
+                    }
+                }
+            }
+        }
+    }
+}
+
 
 // ---------------------------------------------------------------------------
 //
@@ -2089,9 +2567,6 @@
 // initialize the button inputs
 void initButtons(Config &cfg, bool &kbKeys)
 {
-    // presume we'll find no keyboard keys
-    kbKeys = false;
-    
     // presume no shift key
     shiftButton.index = -1;
     
@@ -2208,7 +2683,109 @@
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // D0-DF
      0,  0,  1,  0,  0,  0,  0,  0,  0,  2,  4,  0,  0,  0,  0,  0, // E0-EF
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0  // F0-FF
- };
+};
+ 
+// Keyboard key/joystick button state.  processButtons() uses this to 
+// build the set of key presses to report to the PC based on the logical
+// states of the button iputs.
+struct KeyState
+{
+    KeyState()
+    {
+        // zero all members
+        memset(this, 0, sizeof(*this));
+    }
+    
+    // Keyboard media keys currently pressed.  This is a bit vector in
+    // the format used in our USB keyboard reports (see USBJoystick.cpp).
+    uint8_t mediakeys;
+         
+    // Keyboard modifier (shift) keys currently pressed.  This is a bit 
+    // vector in the format used in our USB keyboard reports (see
+    // USBJoystick.cpp).
+    uint8_t modkeys;
+     
+    // Regular keyboard keys currently pressed.  Each element is a USB
+    // key code, or 0 for empty slots.  Note that the USB report format
+    // theoretically allows a flexible size limit, but the Windows KB
+    // drivers have a fixed limit of 6 simultaneous keys (and won't
+    // accept reports with more), so there's no point in making this
+    // flexible; we'll just use the fixed size dictated by Windows.
+    uint8_t keys[7];
+     
+    // number of valid entries in keys[] array
+    int nkeys;
+     
+    // Joystick buttons pressed, as a bit vector.  Bit n (1 << n)
+    // represents joystick button n, n in 0..31, with 0 meaning 
+    // unpressed and 1 meaning pressed.
+    uint32_t js;
+    
+    
+    // Add a key press.  'typ' is the button type code (ButtonTypeXxx),
+    // and 'val' is the value (the meaning of which varies by type code).
+    void addKey(uint8_t typ, uint8_t val)
+    {
+        // add the key according to the type
+        switch (typ)
+        {
+        case BtnTypeJoystick:
+            // joystick button
+            js |= (1 << (val - 1));
+            break;
+            
+        case BtnTypeKey:
+            // Keyboard key.  The USB keyboard report encodes regular
+            // keys and modifier keys separately, so we need to check
+            // which type we have.  Note that past versions mapped the 
+            // Keyboard Volume Up, Keyboard Volume Down, and Keyboard 
+            // Mute keys to the corresponding Media keys.  We no longer
+            // do this; instead, we have the separate BtnTypeMedia for
+            // explicitly using media keys if desired.
+            if (val >= 0xE0 && val <= 0xE7)
+            {
+                // It's a modifier key.  These are represented in the USB 
+                // reports with a bit mask.  We arrange the mask bits in
+                // the same order as the scan codes, so we can figure the
+                // appropriate bit with a simple shift.
+                modkeys |= (1 << (val - 0xE0));
+            }
+            else
+            {
+                // It's a regular key.  Make sure it's not already in the 
+                // list, and that the list isn't full.  If neither of these 
+                // apply, add the key to the key array.
+                if (nkeys < 7)
+                {
+                    bool found = false;
+                    for (int i = 0 ; i < nkeys ; ++i)
+                    {
+                        if (keys[i] == val)
+                        {
+                            found = true;
+                            break;
+                        }
+                    }
+                    if (!found)
+                        keys[nkeys++] = val;
+                }
+            }
+            break;
+
+        case BtnTypeMedia:
+            // Media control key.  The media keys are mapped in the USB
+            // report to bits, whereas the key codes are specified in the
+            // config with their USB usage numbers.  E.g., the config val
+            // for Media Next Track is 0xB5, but we encode this in the USB
+            // report as bit 0x08.  The mediaKeyMap[] table translates
+            // from the USB usage number to the mask bit.  If the key isn't
+            // among the subset we support, the mapped bit will be zero, so
+            // the "|=" will have no effect and the key will be ignored.
+            mediakeys |= mediaKeyMap[val];
+            break;                                
+        }
+    }
+};
 
 
 // Process the button state.  This sets up the joystick, keyboard, and
@@ -2217,16 +2794,8 @@
 // mapped to special device functions (e.g., Night Mode).
 void processButtons(Config &cfg)
 {
-    // start with an empty list of USB key codes
-    uint8_t modkeys = 0;
-    uint8_t keys[7] = { 0, 0, 0, 0, 0, 0, 0 };
-    int nkeys = 0;
-    
-    // clear the joystick buttons
-    uint32_t newjs = 0;
-    
-    // start with no media keys pressed
-    uint8_t mediakeys = 0;
+    // key state
+    KeyState ks;
     
     // calculate the time since the last run
     uint32_t dt = buttonTimer.read_us();
@@ -2275,6 +2844,9 @@
     ButtonState *bs = buttonState;
     for (int i = 0 ; i < nButtons ; ++i, ++bs)
     {
+        // get the config entry for the button
+        ButtonCfg *bc = &cfg.button[bs->cfgIndex];
+
         // Check the button type:
         //   - shift button
         //   - pulsed button
@@ -2355,45 +2927,78 @@
             // not a pulse switch - the logical state is the same as the physical state
             bs->logState = bs->physState;
         }
+        
+        // Determine if we're going to use the shifted version of the
+        // button.  We're using the shifted version if the shift button
+        // is down AND the button has ANY shifted meaning - a key assignment,
+        // a Night Mode toggle assignment, or an IR code.  If the button 
+        // doesn't have any meaning at all in shifted mode, the base version
+        // of the button applies whether or not the shift button is down.
+        //
+        // Note that the test for Night Mode is a bit tricky.  The shifted
+        // version of the button is the Night Mode toggle if the button matches
+        // the Night Mode button index, AND its flags are set with "toggle
+        // mode ON" (bit 0x02 is on) and "switch mode OFF" (bit 0x01 is off).
+        // That means the button flags & 0x03 must equal 0x02.
+        bool useShift = 
+            (shiftButton.state != 0
+             && (bc->typ2 != BtnTypeNone
+                 || bc->IRCommand2 != 0
+                 || (cfg.nightMode.btn == i+1 && (cfg.nightMode.flags & 0x03) == 0x02)));
+                 
+        // If we're using the shift function, and no other button has used
+        // the shift function yet (shift state 1: "shift button is down but
+        // no one has used the shift function yet"), then we've "consumed"
+        // the shift button press (so go to shift state 2: "shift button has
+        // been used by some other button press that has a shifted meaning").
+        if (useShift && shiftButton.state == 1)
+            shiftButton.state = 2;
 
         // carry out any edge effects from buttons changing states
         if (bs->logState != bs->prevLogState)
         {
-            // check for special key transitions
+            // check to see if this is the Night Mode button
             if (cfg.nightMode.btn == i + 1)
             {
-                // Check the switch type in the config flags.  If flag 0x01 is set,
-                // it's a persistent on/off switch, so the night mode state simply
-                // follows the current state of the switch.  Otherwise, it's a 
-                // momentary button, so each button push (i.e., each transition from
-                // logical state OFF to ON) toggles the current night mode state.
+                // Check the switch type in the config flags.  If flag 0x01 is 
+                // set, it's a persistent on/off switch, so the night mode 
+                // state simply tracks the current state of the switch.  
+                // Otherwise, it's a momentary button, so each button push 
+                // (i.e., each transition from logical state OFF to ON) toggles 
+                // the night mode state.
+                //
+                // Note that the "shift" flag (0x02) has no effect in switch
+                // mode.  Shifting only works for toggle mode.
                 if (cfg.nightMode.flags & 0x01)
                 {
-                    // on/off switch - when the button changes state, change
-                    // night mode to match the new state
+                    // It's an on/off switch.  Night mode simply tracks the
+                    // current switch state.
                     setNightMode(bs->logState);
                 }
                 else
                 {
-                    // Momentary switch - toggle the night mode state when the
-                    // physical button is pushed (i.e., when its logical state
-                    // transitions from OFF to ON).  
+                    // It's a momentary toggle switch.  Toggle the night mode 
+                    // state on each distinct press of the button: that is,
+                    // whenever the button's logical state transitions from 
+                    // OFF to ON.
                     //
-                    // In momentary mode, night mode flag 0x02 makes it the
-                    // shifted version of the button.  In this case, only
-                    // proceed if the shift button is pressed.
-                    bool pressed = bs->logState;
+                    // The "shift" flag (0x02) tells us whether night mode is
+                    // assigned to the shifted or unshifted version of the
+                    // button.
+                    bool pressed;
                     if ((cfg.nightMode.flags & 0x02) != 0)
                     {
-                        // if the shift button is pressed but hasn't been used
-                        // as a shift yet, mark it as used, so that it doesn't
-                        // also generate its own key code on release
-                        if (shiftButton.state == 1)
-                            shiftButton.state = 2;
-                            
-                        // if the shift button isn't even pressed
-                        if (shiftButton.state == 0)
-                            pressed = false;
+                        // Shift bit is set - night mode is assigned to the
+                        // shifted version of the button.  This is a Night
+                        // Mode toggle only if the Shift button is pressed.
+                        pressed = (shiftButton.state != 0);
+                    }
+                    else
+                    {
+                        // No shift bit - night mode is assigned to the
+                        // regular unshifted button.  The button press only
+                        // applies if the Shift button is NOT pressed.
+                        pressed = (shiftButton.state == 0);
                     }
                     
                     // if it's pressed (even after considering the shift mode),
@@ -2403,6 +3008,11 @@
                 }
             }
             
+            // press or release IR virtual keys on key state changes
+            uint8_t irc = useShift ? bc->IRCommand2 : bc->IRCommand;
+            if (irc != 0)
+                IR_buttonChange(irc, bs->logState);
+            
             // remember the new state for comparison on the next run
             bs->prevLogState = bs->logState;
         }
@@ -2413,131 +3023,53 @@
         {
             // Get the key type and code.  Start by assuming that we're
             // going to use the normal unshifted meaning.
-            ButtonCfg *bc = &cfg.button[bs->cfgIndex];
-            uint8_t typ = bc->typ;
-            uint8_t val = bc->val;
-            
-            // If the shift button is down, check for a shifted meaning.
-            if (shiftButton.state)
+            uint8_t typ, val;
+            if (useShift)
             {
-                // assume there's no shifted meaning
-                bool useShift = false;
-                
-                // If the button has a shifted meaning, use that.  The
-                // meaning might be a keyboard key or joystick button,
-                // but it could also be as the Night Mode toggle.
-                //
-                // The condition to check if it's the Night Mode toggle
-                // is a little complicated.  First, the easy part: our
-                // button index has to match the Night Mode button index.
-                // Now the hard part: the Night Mode button flags have
-                // to be set to 0x01 OFF and 0x02 ON: toggle mode (not
-                // switch mode, 0x01), and shift mode, 0x02.  So AND the
-                // flags with 0x03 to get these two bits, and check that
-                // the result is 0x02, meaning that only shift mode is on.
-                if (bc->typ2 != BtnTypeNone)
-                {
-                    // there's a shifted key assignment - use it
-                    typ = bc->typ2;
-                    val = bc->val2;
-                    useShift = true;
-                }
-                else if (cfg.nightMode.btn == i+1 
-                         && (cfg.nightMode.flags & 0x03) == 0x02)
-                {
-                    // shift+button = night mode toggle
-                    typ = BtnTypeNone;
-                    val = 0;
-                    useShift = true;
-                }
-                
-                // If there's a shifted meaning, advance the shift
-                // button state from 1 to 2 if applicable.  This signals
-                // that we've "consumed" the shift button press as the
-                // shift button, so it shouldn't generate its own key
-                // code event when released.
-                if (useShift && shiftButton.state == 1)
-                    shiftButton.state = 2;
+                typ = bc->typ2;
+                val = bc->val2;
             }
-            
+            else
+            {
+                typ = bc->typ;
+                val = bc->val;
+            }
+        
             // We've decided on the meaning of the button, so process
             // the keyboard or joystick event.
-            switch (typ)
-            {
-            case BtnTypeJoystick:
-                // joystick button
-                newjs |= (1 << (val - 1));
-                break;
-                
-            case BtnTypeKey:
-                // Keyboard key.  The USB keyboard report encodes regular
-                // keys and modifier keys separately, so we need to check
-                // which type we have.  Note that past versions mapped the 
-                // Keyboard Volume Up, Keyboard Volume Down, and Keyboard 
-                // Mute keys to the corresponding Media keys.  We no longer
-                // do this; instead, we have the separate BtnTypeMedia for
-                // explicitly using media keys if desired.
-                if (val >= 0xE0 && val <= 0xE7)
-                {
-                    // It's a modifier key.  These are represented in the USB 
-                    // reports with a bit mask.  We arrange the mask bits in
-                    // the same order as the scan codes, so we can figure the
-                    // appropriate bit with a simple shift.
-                    modkeys |= (1 << (val - 0xE0));
-                }
-                else
-                {
-                    // It's a regular key.  Make sure it's not already in the 
-                    // list, and that the list isn't full.  If neither of these 
-                    // apply, add the key to the key array.
-                    if (nkeys < 7)
-                    {
-                        bool found = false;
-                        for (int j = 0 ; j < nkeys ; ++j)
-                        {
-                            if (keys[j] == val)
-                            {
-                                found = true;
-                                break;
-                            }
-                        }
-                        if (!found)
-                            keys[nkeys++] = val;
-                    }
-                }
-                break;
-
-            case BtnTypeMedia:
-                // Media control key.  The media keys are mapped in the USB
-                // report to bits, whereas the key codes are specified in the
-                // config with their USB usage numbers.  E.g., the config val
-                // for Media Next Track is 0xB5, but we encode this in the USB
-                // report as bit 0x08.  The mediaKeyMap[] table translates
-                // from the USB usage number to the mask bit.  If the key isn't
-                // among the subset we support, the mapped bit will be zero, so
-                // the "|=" will have no effect and the key will be ignored.
-                mediakeys |= mediaKeyMap[val];
-                break;                                
-            }
+            ks.addKey(typ, val);
         }
     }
-
-    // check for joystick button changes
-    if (jsButtons != newjs)
-        jsButtons = newjs;
-    
-    // Check for changes to the keyboard keys
-    if (kbState.data[0] != modkeys
-        || kbState.nkeys != nkeys
-        || memcmp(keys, &kbState.data[2], 6) != 0)
+    
+    // If an IR input command is in effect, add the IR command's
+    // assigned key, if any.  If we're in an IR key gap, don't include
+    // the IR key.
+    if (IRCommandIn != 0 && !IRKeyGap)
+    {
+        IRCommandCfg &irc = cfg.IRCommand[IRCommandIn - 1];
+        ks.addKey(irc.keytype, irc.keycode);
+    }
+    
+    // We're finished building the new key state.  Update the global
+    // key state variables to reflect the new state. 
+    
+    // set the new joystick buttons (no need to check for changes, as we
+    // report these on every joystick report whether they changed or not)
+    jsButtons = ks.js;
+    
+    // check for keyboard key changes (we only send keyboard reports when
+    // something changes)
+    if (kbState.data[0] != ks.modkeys
+        || kbState.nkeys != ks.nkeys
+        || memcmp(ks.keys, &kbState.data[2], 6) != 0)
     {
         // we have changes - set the change flag and store the new key data
         kbState.changed = true;
-        kbState.data[0] = modkeys;
-        if (nkeys <= 6) {
+        kbState.data[0] = ks.modkeys;
+        if (ks.nkeys <= 6) {
             // 6 or fewer simultaneous keys - report the key codes
-            kbState.nkeys = nkeys;
-            memcpy(&kbState.data[2], keys, 6);
+            kbState.nkeys = ks.nkeys;
+            memcpy(&kbState.data[2], ks.keys, 6);
         }
         else {
             // more than 6 simultaneous keys - report rollover (all '1' key codes)
@@ -2546,11 +3078,13 @@
         }
     }        
     
-    // Check for changes to media keys
-    if (mediaState.data != mediakeys)
+    // check for media key changes (we only send media key reports when
+    // something changes)
+    if (mediaState.data != ks.mediakeys)
     {
+        // we have changes - set the change flag and store the new key data
         mediaState.changed = true;
-        mediaState.data = mediakeys;
+        mediaState.data = ks.mediakeys;
     }
 }
 
@@ -2811,12 +3345,34 @@
 // MMA8451Q.  This class encapsulates an interrupt handler and 
 // automatic calibration.
 //
-// We install an interrupt handler on the accelerometer "data ready" 
-// interrupt to ensure that we fetch each sample immediately when it
-// becomes available.  The accelerometer data rate is fairly high
-// (800 Hz), so it's not practical to keep up with it by polling.
-// Using an interrupt handler lets us respond quickly and read
-// every sample.
+// We collect data at the device's maximum rate of 800kHz (one sample 
+// every 1.25ms).  To keep up with the high data rate, we use the 
+// device's internal FIFO, and drain the FIFO by polling on each 
+// iteration of our main application loop.  In the past, we used an
+// interrupt handler to read the device immediately on the arrival of
+// each sample, but this created too much latency for the IR remote
+// receiver, due to the relatively long time it takes to transfer the
+// accelerometer readings via I2C.  The device's on-board FIFO can
+// store up to 32 samples, which gives us up to about 40ms between
+// polling iterations before the buffer overflows.  Our main loop runs
+// in under 2ms, so we can easily keep the FIFO far from overflowing.
+//
+// The MMA8451Q has three range modes, +/- 2G, 4G, and 8G.  The ADC
+// sample is the same bit width (14 bits) in all modes, so the higher
+// dynamic range modes trade physical precision for range.  For our
+// purposes, precision is more important than range, so we use the
+// +/-2G mode.  Further, our joystick range is calibrated for only
+// +/-1G.  This was unintentional on my part; I didn't look at the
+// MMA8451Q library closely enough to realize it was normalizing to
+// actual "G" units, and assumed that it was normalizing to a -1..+1 
+// scale.  In practice, a +/-1G scale seems perfectly adequate for
+// virtual pinball use, so I'm sticking with that range for now.  But
+// there might be some benefit in renormalizing to a +/-2G range, in
+// that it would allow for higher dynamic range for very hard nudges.
+// Everyone would have to tweak their nudge sensitivity in VP if I
+// made that change, though, so I'm keeping it as is for now; it would
+// be best to make it a config option ("accelerometer high dynamic range") 
+// rather than change it across the board.
 //
 // We automatically calibrate the accelerometer so that it's not
 // necessary to get it exactly level when installing it, and so
@@ -2850,43 +3406,46 @@
 // accelerometer input history item, for gathering calibration data
 struct AccHist
 {
-    AccHist() { x = y = d = 0.0; xtot = ytot = 0.0; cnt = 0; }
-    void set(float x, float y, AccHist *prv)
+    AccHist() { x = y = dsq = 0; xtot = ytot = 0; cnt = 0; }
+    void set(int x, int y, AccHist *prv)
     {
         // save the raw position
         this->x = x;
         this->y = y;
-        this->d = distance(prv);
+        this->dsq = distanceSquared(prv);
     }
     
     // reading for this entry
-    float x, y;
-    
-    // distance from previous entry
-    float d;
+    int x, y;
+    
+    // (distance from previous entry) squared
+    int dsq;
     
     // total and count of samples averaged over this period
-    float xtot, ytot;
+    int xtot, ytot;
     int cnt;
 
-    void clearAvg() { xtot = ytot = 0.0; cnt = 0; }    
-    void addAvg(float x, float y) { xtot += x; ytot += y; ++cnt; }
-    float xAvg() const { return xtot/cnt; }
-    float yAvg() const { return ytot/cnt; }
-    
-    float distance(AccHist *p)
-        { return sqrt(square(p->x - x) + square(p->y - y)); }
+    void clearAvg() { xtot = ytot = 0; cnt = 0; }    
+    void addAvg(int x, int y) { xtot += x; ytot += y; ++cnt; }
+    int xAvg() const { return xtot/cnt; }
+    int yAvg() const { return ytot/cnt; }
+    
+    int distanceSquared(AccHist *p)
+        { return square(p->x - x) + square(p->y - y); }
 };
 
 // accelerometer wrapper class
 class Accel
 {
 public:
-    Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin)
-        : mma_(sda, scl, i2cAddr), intIn_(irqPin)
+    Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin, int range)
+        : mma_(sda, scl, i2cAddr)        
     {
         // remember the interrupt pin assignment
         irqPin_ = irqPin;
+        
+        // remember the range
+        range_ = range;
 
         // reset and initialize
         reset();
@@ -2895,138 +3454,130 @@
     void reset()
     {
         // clear the center point
-        cx_ = cy_ = 0.0;
+        cx_ = cy_ = 0;
         
-        // start the calibration timer
+        // start the auto-centering timer
         tCenter_.start();
         iAccPrv_ = nAccPrv_ = 0;
         
         // reset and initialize the MMA8451Q
         mma_.init();
+        
+        // set the range
+        mma_.setRange(
+            range_ == AccelRange4G ? 4 :
+            range_ == AccelRange8G ? 8 :
+            2);
                 
-        // set the initial integrated velocity reading to zero
-        vx_ = vy_ = 0;
-        
-        // set up our accelerometer interrupt handling
-        intIn_.rise(this, &Accel::isr);
-        mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2);
+        // set the average accumulators to zero
+        xSum_ = ySum_ = 0;
+        nSum_ = 0;
         
         // read the current registers to clear the data ready flag
         mma_.getAccXYZ(ax_, ay_, az_);
-
-        // start our timers
-        tGet_.start();
-        tInt_.start();
     }
     
-    void disableInterrupts()
+    void poll()
     {
-        mma_.clearInterruptMode();
+        // read samples until we clear the FIFO
+        while (mma_.getFIFOCount() != 0)
+        {
+            int x, y, z;
+            mma_.getAccXYZ(x, y, z);
+            
+            // add the new reading to the running total for averaging
+            xSum_ += (x - cx_);
+            ySum_ += (y - cy_);
+            ++nSum_;
+            
+            // store the updates
+            ax_ = x;
+            ay_ = y;
+            az_ = z;
+        }
     }
-    
+
     void get(int &x, int &y) 
     {
-         // disable interrupts while manipulating the shared data
-         __disable_irq();
-         
-         // read the shared data and store locally for calculations
-         float ax = ax_, ay = ay_;
-         float vx = vx_, vy = vy_;
-         
-         // reset the velocity sum for the next run
-         vx_ = vy_ = 0;
-
-         // get the time since the last get() sample
-         int dtus = tGet_.read_us();
-         tGet_.reset();
-         
-         // done manipulating the shared data
-         __enable_irq();
-         
-         // adjust the readings for the integration time
-         float dt = dtus/1000000.0f;
-         vx /= dt;
-         vy /= dt;
+        // read the shared data and store locally for calculations
+        int ax = ax_, ay = ay_;
+        int xSum = xSum_, ySum = ySum_;
+        int nSum = nSum_;
          
-         // add this sample to the current calibration interval's running total
-         AccHist *p = accPrv_ + iAccPrv_;
-         p->addAvg(ax, ay);
-
-         // check for auto-centering every so often
-         if (tCenter_.read_us() > 1000000)
-         {
-             // add the latest raw sample to the history list
-             AccHist *prv = p;
-             iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv;
-             p = accPrv_ + iAccPrv_;
-             p->set(ax, ay, prv);
-
-             // if we have a full complement, check for stability
-             if (nAccPrv_ >= maxAccPrv)
-             {
-                 // check if we've been stable for all recent samples
-                 static const float accTol = .01f;
-                 AccHist *p0 = accPrv_;
-                 if (p0[0].d < accTol
-                     && p0[1].d < accTol
-                     && p0[2].d < accTol
-                     && p0[3].d < accTol
-                     && p0[4].d < accTol)
-                 {
-                     // Figure the new calibration point as the average of
-                     // the samples over the rest period
-                     cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5.0f;
-                     cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5.0f;
-                 }
-             }
-             else
-             {
-                // not enough samples yet; just up the count
-                ++nAccPrv_;
-             }
+        // reset the average accumulators for the next run
+        xSum_ = ySum_ = 0;
+        nSum_ = 0;
+
+        // add this sample to the current calibration interval's running total
+        AccHist *p = accPrv_ + iAccPrv_;
+        p->addAvg(ax, ay);
+
+        // check for auto-centering every so often
+        if (tCenter_.read_us() > 1000000)
+        {
+            // add the latest raw sample to the history list
+            AccHist *prv = p;
+            iAccPrv_ = (iAccPrv_ + 1);
+            if (iAccPrv_ >= maxAccPrv)
+               iAccPrv_ = 0;
+            p = accPrv_ + iAccPrv_;
+            p->set(ax, ay, prv);
+
+            // if we have a full complement, check for stability
+            if (nAccPrv_ >= maxAccPrv)
+            {
+                // check if we've been stable for all recent samples
+                static const int accTol = 164*164;  // 1% of range, squared
+                AccHist *p0 = accPrv_;
+                if (p0[0].dsq < accTol
+                    && p0[1].dsq < accTol
+                    && p0[2].dsq < accTol
+                    && p0[3].dsq < accTol
+                    && p0[4].dsq < accTol)
+                {
+                    // Figure the new calibration point as the average of
+                    // the samples over the rest period
+                    cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5;
+                    cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5;
+                }
+            }
+            else
+            {
+               // not enough samples yet; just up the count
+               ++nAccPrv_;
+            }
              
-             // clear the new item's running totals
-             p->clearAvg();
+            // clear the new item's running totals
+            p->clearAvg();
             
-             // reset the timer
-             tCenter_.reset();
-             
-             // If we haven't seen an interrupt in a while, do an explicit read to
-             // "unstick" the device.  The device can become stuck - which is to say,
-             // it will stop delivering data-ready interrupts - if we fail to service
-             // one data-ready interrupt before the next one occurs.  Reading a sample
-             // will clear up this overrun condition and allow normal interrupt
-             // generation to continue.
-             //
-             // Note that this stuck condition *shouldn't* ever occur, because if only
-             // happens if we're spending a long period with interrupts disabled (in
-             // a critical section or in another interrupt handler), which will likely
-             // cause other, worse problems beyond the sticky accelerometer.  Even so, 
-             // it's easy to detect and correct, so we'll do so for the sake of making 
-             // the system a little more fault-tolerant.
-             if (tInt_.read_us() > 1000000)
-             {
-                float x, y, z;
-                mma_.getAccXYZ(x, y, z);
-             }
-         }
+            // reset the timer
+            tCenter_.reset();
+        }
          
-         // report our integrated velocity reading in x,y
-         x = rawToReport(vx);
-         y = rawToReport(vy);
+        // report our integrated velocity reading in x,y
+        x = rawToReport(xSum/nSum);
+        y = rawToReport(ySum/nSum);
          
 #ifdef DEBUG_PRINTF
-         if (x != 0 || y != 0)        
-             printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
+        if (x != 0 || y != 0)        
+            printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
 #endif
-     }    
+    }    
          
 private:
     // adjust a raw acceleration figure to a usb report value
-    int rawToReport(float v)
+    int rawToReport(int v)
     {
-        // scale to the joystick report range and round to integer
-        int i = int(round(v*JOYMAX));
+        // Scale to the joystick report range.  The accelerometer
+        // readings use the native 14-bit signed integer representation,
+        // so their scale is 2^13.
+        //
+        // The 1G range is special: it uses the 2G native hardware range,
+        // but rescales the result to a 1G range for the joystick reports.
+        // So for that mode, we divide by 4096 rather than 8192.  All of
+        // the other modes map use the hardware scaling directly.
+        int i = v*JOYMAX;
+        i = (range_ == AccelRange1G ? i/4096 : i/8192);
         
         // if it's near the center, scale it roughly as 20*(i/20)^2,
         // to suppress noise near the rest position
@@ -3038,56 +3589,32 @@
         return (i > 20 || i < -20 ? i : filter[i+20]);
     }
 
-    // interrupt handler
-    void isr()
-    {
-        // Read the axes.  Note that we have to read all three axes
-        // (even though we only really use x and y) in order to clear
-        // the "data ready" status bit in the accelerometer.  The
-        // interrupt only occurs when the "ready" bit transitions from
-        // off to on, so we have to make sure it's off.
-        float x, y, z;
-        mma_.getAccXYZ(x, y, z);
-        
-        // calculate the time since the last interrupt
-        float dt = tInt_.read();
-        tInt_.reset();
-
-        // integrate the time slice from the previous reading to this reading
-        vx_ += (x + ax_ - 2*cx_)*dt/2;
-        vy_ += (y + ay_ - 2*cy_)*dt/2;
-        
-        // store the updates
-        ax_ = x;
-        ay_ = y;
-        az_ = z;
-    }
-    
     // underlying accelerometer object
     MMA8451Q mma_;
     
-    // last raw acceleration readings
-    float ax_, ay_, az_;
-    
-    // integrated velocity reading since last get()
-    float vx_, vy_;
+    // last raw acceleration readings, on the device's signed 14-bit 
+    // scale -8192..+8191
+    int ax_, ay_, az_;
+    
+    // running sum of readings since last get()
+    int xSum_, ySum_;
+    
+    // number of readings since last get()
+    int nSum_;
         
-    // timer for measuring time between get() samples
-    Timer tGet_;
-    
-    // timer for measuring time between interrupts
-    Timer tInt_;
-
     // Calibration reference point for accelerometer.  This is the
     // average reading on the accelerometer when in the neutral position
     // at rest.
-    float cx_, cy_;
-
-    // timer for atuo-centering
+    int cx_, cy_;
+    
+    // range (AccelRangeXxx value, from config.h)
+    uint8_t range_;
+
+    // atuo-centering timer
     Timer tCenter_;
 
     // Auto-centering history.  This is a separate history list that
-    // records results spaced out sparesely over time, so that we can
+    // records results spaced out sparsely over time, so that we can
     // watch for long-lasting periods of rest.  When we observe nearly
     // no motion for an extended period (on the order of 5 seconds), we
     // take this to mean that the cabinet is at rest in its neutral 
@@ -3104,9 +3631,6 @@
     
     // interurupt pin name
     PinName irqPin_;
-    
-    // interrupt router
-    InterruptIn intIn_;
 };
 
 // ---------------------------------------------------------------------------
@@ -3274,25 +3798,37 @@
 //   so we don't want to push the button on a TV that's already on.
 //   
 
-// Current PSU2 state:
+// Current PSU2 power state:
 //   1 -> default: latch was on at last check, or we haven't checked yet
 //   2 -> latch was off at last check, SET pulsed high
 //   3 -> SET pulsed low, ready to check status
 //   4 -> TV timer countdown in progress
 //   5 -> TV relay on
+//   6 -> sending IR signals designed as TV ON signals
 uint8_t psu2_state = 1;
 
 // TV relay state.  The TV relay can be controlled by the power-on
 // timer and directly from the PC (via USB commands), so keep a
 // separate state for each:
-//
 //   0x01 -> turned on by power-on timer
 //   0x02 -> turned on by USB command
 uint8_t tv_relay_state = 0x00;
 const uint8_t TV_RELAY_POWERON = 0x01;
 const uint8_t TV_RELAY_USB     = 0x02;
 
-// TV ON switch relay control
+// TV ON IR command state.  When the main PSU2 power state reaches
+// the IR phase, we use this sub-state counter to send the TV ON
+// IR signals.  We initialize to state 0 when the main state counter
+// reaches the IR step.  In state 0, we start transmitting the first
+// (lowest numbered) IR command slot marked as containing a TV ON
+// code, and advance to state 1.  In state 1, we check to see if
+// the transmitter is still sending; if so, we do nothing, if so
+// we start transmitting the second TV ON code and advance to state
+// 2.  Continue until we run out of TV ON IR codes, at which point
+// we advance to the next main psu2_state step.
+uint8_t tvon_ir_state = 0;
+
+// TV ON switch relay control output pin
 DigitalOut *tv_relay;
 
 // PSU2 power sensing circuit connections
@@ -3313,12 +3849,27 @@
         tv_relay->write(tv_relay_state != 0);
 }
 
-// Timer interrupt
-Ticker tv_ticker;
-float tv_delay_time;
-void TVTimerInt()
+// PSU2 Status update routine.  The main loop calls this from time 
+// to time to update the power sensing state and carry out TV ON 
+// functions.
+Timer powerStatusTimer;
+uint32_t tv_delay_time_us;
+void powerStatusUpdate(Config &cfg)
 {
-    // time since last state change
+    // Only update every 1/4 second or so.  Note that if the PSU2
+    // circuit isn't configured, the initialization routine won't 
+    // start the timer, so it'll always read zero and we'll always 
+    // skip this whole routine.
+    if (powerStatusTimer.read_us() < 250000)
+        return;
+        
+    // reset the update timer for next time
+    powerStatusTimer.reset();
+    
+    // TV ON timer.  We start this timer when we detect a change
+    // in the PSU2 status from OFF to ON.  When the timer reaches
+    // the configured TV ON delay time, and the PSU2 power is still
+    // on, we'll trigger the TV ON relay and send the TV ON IR codes.
     static Timer tv_timer;
 
     // Check our internal state
@@ -3338,6 +3889,7 @@
             // try setting the latch
             psu2_status_set->write(1);
         }
+        powerTimerDiagState = 0;
         break;
         
     case 2:
@@ -3345,6 +3897,7 @@
         // the latch.  Drop the SET signal and go to CHECK state.
         psu2_status_set->write(0);
         psu2_state = 3;
+        powerTimerDiagState = 0;
         break;
         
     case 3:
@@ -3362,7 +3915,6 @@
             
             // start the power timer diagnostic flashes
             powerTimerDiagState = 2;
-            diagLED();
         }
         else
         {
@@ -3375,53 +3927,136 @@
         break;
         
     case 4:
-        // TV timer countdown in progress.  If we've reached the
-        // delay time, pulse the relay.
-        if (tv_timer.read() >= tv_delay_time)
+        // TV timer countdown in progress.  The latch has to stay on during
+        // the countdown; if the latch turns off, PSU2 power must have gone
+        // off again before the countdown finished.
+        if (!psu2_status_sense->read())
+        {
+            // power is off - start a new check cycle
+            psu2_status_set->write(1);
+            psu2_state = 2;
+            break;
+        }
+        
+        // Flash the power time diagnostic every two cycles
+        powerTimerDiagState = (powerTimerDiagState + 1) & 0x03;
+        
+        // if we've reached the delay time, pulse the relay
+        if (tv_timer.read_us() >= tv_delay_time_us)
         {
             // turn on the relay for one timer interval
             tvRelayUpdate(TV_RELAY_POWERON, true);
             psu2_state = 5;
+            
+            // show solid blue on the diagnostic LED while the relay is on
+            powerTimerDiagState = 2;
         }
-        
-        // flash the power time diagnostic every two interrupts
-        powerTimerDiagState = (powerTimerDiagState + 1) & 0x03;
-        diagLED();
         break;
         
     case 5:
         // TV timer relay on.  We pulse this for one interval, so
-        // it's now time to turn it off and return to the default state.
+        // it's now time to turn it off.
         tvRelayUpdate(TV_RELAY_POWERON, false);
+        
+        // Proceed to sending any TV ON IR commands
+        psu2_state = 6;
+        tvon_ir_state = 0;
+    
+        // diagnostic LEDs off for now
+        powerTimerDiagState = 0;
+        break;
+        
+    case 6:        
+        // Sending TV ON IR signals.  Start with the assumption that
+        // we have no IR work to do, in which case we're done with the
+        // whole TV ON sequence.  So by default return to state 1.
         psu2_state = 1;
+        powerTimerDiagState = 0;
         
-        // done with the diagnostic flashes
-        powerTimerDiagState = 0;
-        diagLED();
+        // If we have an IR emitter, check for TV ON IR commands
+        if (ir_tx != 0)
+        {
+            // check to see if the last transmission is still in progress
+            if (ir_tx->isSending())
+            {
+                // We're still sending the last transmission.  Stay in
+                // state 6.
+                psu2_state = 6;
+                powerTimerDiagState = 4;
+                break;
+            }
+                
+            // The last transmission is done, so check for a new one.
+            // Look for the Nth TV ON IR slot, where N is our state
+            // number.
+            for (int i = 0, n = 0 ; i < MAX_IR_CODES ; ++i)
+            {
+                // is this a TV ON command?
+                if ((cfg.IRCommand[i].flags & IRFlagTVON) != 0)
+                {
+                    // It's a TV ON command - check if it's the one we're
+                    // looking for.
+                    if (n == tvon_ir_state)
+                    {
+                        // It's the one.  Start transmitting it by
+                        // pushing its virtual button.
+                        int vb = IRConfigSlotToVirtualButton[i];
+                        ir_tx->pushButton(vb, true);
+                        
+                        // Pushing the button starts transmission, and once
+                        // started, the transmission will run to completion
+                        // even if the button is no longer pushed.  So we
+                        // can immediately un-push the button, since we only
+                        // need to send the code once.
+                        ir_tx->pushButton(vb, false);
+                        
+                        // Advance to the next TV ON IR state, where we'll
+                        // await the end of this transmission and move on to
+                        // the next one.
+                        psu2_state = 6;
+                        tvon_ir_state++;
+                        break;
+                    }
+                    
+                    // it's not ours - count it and keep looking
+                    ++n;
+                }
+            }
+        }
         break;
     }
+    
+    // update the diagnostic LEDs
+    diagLED();
 }
 
-// Start the TV ON checker.  If the status sense circuit is enabled in
-// the configuration, we'll set up the pin connections and start the
-// interrupt handler that periodically checks the status.  Does nothing
-// if any of the pins are configured as NC.
-void startTVTimer(Config &cfg)
+// Start the power status timer.  If the status sense circuit is enabled 
+// in the configuration, we'll set up the pin connections and start the
+// timer for our periodic status checks.  Does nothing if any of the pins 
+// are configured as NC.
+void startPowerStatusTimer(Config &cfg)
 {
     // only start the timer if the pins are configured and the delay
     // time is nonzero
-    if (cfg.TVON.delayTime != 0
-        && cfg.TVON.statusPin != 0xFF 
-        && cfg.TVON.latchPin != 0xFF 
-        && cfg.TVON.relayPin != 0xFF)
+    powerStatusTimer.reset();
+    if (cfg.TVON.statusPin != 0xFF 
+        && cfg.TVON.latchPin != 0xFF)
     {
+        // set up the power sensing circuit connections
         psu2_status_sense = new DigitalIn(wirePinName(cfg.TVON.statusPin));
         psu2_status_set = new DigitalOut(wirePinName(cfg.TVON.latchPin));
-        tv_relay = new DigitalOut(wirePinName(cfg.TVON.relayPin));
-        tv_delay_time = cfg.TVON.delayTime/100.0f;
-    
-        // Set up our time routine to run every 1/4 second.  
-        tv_ticker.attach(&TVTimerInt, 0.25);
+        
+        // if there's a TV ON relay, set up its control pin
+        if (cfg.TVON.relayPin != 0xFF)
+            tv_relay = new DigitalOut(wirePinName(cfg.TVON.relayPin));
+            
+        // Set the TV ON delay time.  We store the time internally in
+        // microseconds, but the configuration stores it in units of
+        // 1/100 second = 10ms = 10000us.
+        tv_delay_time_us = cfg.TVON.delayTime * 10000;;
+    
+        // Start the TV timer
+        powerStatusTimer.start();
     }
 }
 
@@ -3482,6 +4117,18 @@
 //
 NVM nvm;
 
+// Flag: configuration save requested.  The USB command message handler
+// sets this flag when a command is sent requesting the save.  We don't
+// do the save inline in the command handler, but handle it on the next 
+// main loop iteration.
+const uint8_t SAVE_CONFIG_ONLY = 1;
+const uint8_t SAVE_CONFIG_AND_REBOOT = 2;
+uint8_t saveConfigPending = 0;
+
+// If saveConfigPending == SAVE_CONFIG_AND_REBOOT, this specifies the
+// delay time in seconds before rebooting.
+uint8_t saveConfigRebootTime;
+
 // For convenience, a macro for the Config part of the NVM structure
 #define cfg (nvm.d.c)
 
@@ -3499,26 +4146,10 @@
 }
 flash_nvm_memory __attribute__ ((aligned(SECTOR_SIZE))) = { };
 
-// figure the flash address as a pointer along with the number of sectors
-// required to store the structure
-NVM *configFlashAddr(int &addr, int &numSectors)
-{
-    // figure how many flash sectors we span, rounding up to whole sectors
-    numSectors = (sizeof(NVM) + SECTOR_SIZE - 1)/SECTOR_SIZE;
-
-    // figure the address - this is the highest flash address where the
-    // structure will fit with the start aligned on a sector boundary
-    addr = (int)&flash_nvm_memory;
-    
-    // return the address as a pointer
-    return (NVM *)addr;
-}
-
 // figure the flash address as a pointer
 NVM *configFlashAddr()
 {
-    int addr, numSectors;
-    return configFlashAddr(addr, numSectors);
+    return (NVM *)&flash_nvm_memory;
 }
 
 // Load the config from flash.  Returns true if a valid non-default
@@ -3570,8 +4201,7 @@
     waitPlungerIdle();
     
     // get the config block location in the flash memory
-    int addr, sectors;
-    configFlashAddr(addr, sectors);
+    uint32_t addr = uint32_t(configFlashAddr());
     
     // loop until we save it successfully
     for (int i = 0 ; i < 5 ; ++i)
@@ -3588,10 +4218,10 @@
         // verify the data
         if (nvm.verify(addr))
         {
-            // show a diagnostic success flash
-            for (int j = 0 ; j < 3 ; ++j)
+            // show a diagnostic success flash (rapid green)
+            for (int j = 0 ; j < 4 ; ++j)
             {
-                diagLED(0, 1, 1);
+                diagLED(0, 1, 0);
                 wait_us(50000);
                 diagLED(0, 0, 0);
                 wait_us(50000);
@@ -3704,8 +4334,10 @@
 // Turn night mode on or off
 static void setNightMode(bool on)
 {
-    // set the new night mode flag in the noisy output class
-    nightMode = on;
+    // Set the new night mode flag in the noisy output class.  Note
+    // that we use the status report bit flag value 0x02 when on, so
+    // that we can just '|' this into the overall status bits.
+    nightMode = on ? 0x02 : 0x00;
     
     // update the special output pin that shows the night mode state
     int port = int(cfg.nightMode.port) - 1;
@@ -4890,6 +5522,7 @@
 #define if_msg_valid(test)  if (test)
 #define v_byte(var, ofs)    cfg.var = data[ofs]
 #define v_ui16(var, ofs)    cfg.var = wireUI16(data+(ofs))
+#define v_ui32(var, ofs)    cfg.var = wireUI32(data+(ofs))
 #define v_pin(var, ofs)     cfg.var = wirePinName(data[ofs])
 #define v_byte_ro(val, ofs) // ignore read-only variables on SET
 #define v_ui32_ro(val, ofs) // ignore read-only variables on SET
@@ -4901,6 +5534,7 @@
 #undef if_msg_valid
 #undef v_byte
 #undef v_ui16
+#undef v_ui32
 #undef v_pin
 #undef v_byte_ro
 #undef v_ui32_ro
@@ -4911,6 +5545,7 @@
 #define if_msg_valid(test)
 #define v_byte(var, ofs)    data[ofs] = cfg.var
 #define v_ui16(var, ofs)    ui16Wire(data+(ofs), cfg.var)
+#define v_ui32(var, ofs)    ui32Wire(data+(ofs), cfg.var)
 #define v_pin(var, ofs)     pinNameWire(data+(ofs), cfg.var)
 #define v_byte_ro(val, ofs) data[ofs] = (val)
 #define v_ui32_ro(val, ofs) ui32Wire(data+(ofs), val);
@@ -4986,12 +5621,9 @@
                 cfg.psUnitNo = newUnitNo;
                 cfg.plunger.enabled = data[3] & 0x01;
                 
-                // save the configuration
-                saveConfigToFlash();
-                
-                // reboot if necessary
-                if (needReset)
-                    reboot(js);
+                // set the flag to do the save
+                saveConfigPending = needReset ? SAVE_CONFIG_AND_REBOOT : SAVE_CONFIG_ONLY;
+                saveConfigRebootTime = 0;
             }
             break;
             
@@ -5035,13 +5667,10 @@
             break;
             
         case 6:
-            // 6 = Save configuration to flash.
-            saveConfigToFlash();
-            
-            // before disconnecting, pause for the delay time specified in
-            // the parameter byte (in seconds)
-            rebootTime_us = data[2] * 1000000L;
-            rebootTimer.start();
+            // 6 = Save configuration to flash.  Reboot after the delay
+            // time in seconds given in data[2].
+            saveConfigPending = SAVE_CONFIG_AND_REBOOT;
+            saveConfigRebootTime = data[2];
             break;
             
         case 7:
@@ -5091,7 +5720,20 @@
             break;
             
         case 12:
-            // Unused
+            // 12 = Learn IR code.  This enters IR learning mode.  While
+            // in learning mode, we report raw IR signals and the first IR
+            // command decoded through the special IR report format.  IR
+            // learning mode automatically ends after a timeout expires if
+            // no command can be decoded within the time limit.
+            
+            // enter IR learning mode
+            IRLearningMode = 1;
+            
+            // cancel any regular IR input in progress
+            IRCommandIn = 0;
+            
+            // reset and start the learning mode timeout timer
+            IRTimer.reset();
             break;
             
         case 13:
@@ -5245,7 +5887,8 @@
 {
     // say hello to the debug console, in case it's connected
     printf("\r\nPinscape Controller starting\r\n");
-
+    
+    
     // debugging: print memory config info
     //    -> no longer very useful, since we use our own custom malloc/new allocator (see xmalloc() above)
     // {int *a = new int; printf("Stack=%lx, heap=%lx, free=%ld\r\n", (long)&a, (long)a, (long)&a - (long)a);} 
@@ -5313,12 +5956,20 @@
     // start the TLC5940 refresh cycle clock
     if (tlc5940 != 0)
         tlc5940->start();
-        
-    // start the TV timer, if applicable
-    startTVTimer(cfg);
+
+    // 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
+    // USB keyboard interface.
+    bool kbKeys = false;
+
+    // set up the IR remote control emitter & receiver, if present
+    init_IR(cfg, kbKeys);
+
+    // start the power status time, if applicable
+    startPowerStatusTimer(cfg);
 
     // initialize the button input ports
-    bool kbKeys = false;
     initButtons(cfg, kbKeys);
     
     // Create the joystick USB client.  Note that the USB vendor/product ID
@@ -5349,9 +6000,16 @@
             connFlashTimer.reset();
         }
 
+        // If we've been disconnected for more than the reboot timeout,
+        // reboot.  Some PCs won't reconnect if we were left plugged in
+        // during a power cycle on the PC, but fortunately a reboot on
+        // the KL25Z will make the host notice us and trigger a reconnect.
         if (cfg.disconnectRebootTimeout != 0 
             && connTimeoutTimer.read() > cfg.disconnectRebootTimeout)
             reboot(js, false, 0);
+            
+        // update the PSU2 power sensing status
+        powerStatusUpdate(cfg);
     }
     
     // we're now connected to the host
@@ -5418,7 +6076,8 @@
     acTimer.start();
     
     // create the accelerometer object
-    Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
+    Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, 
+        MMA8451_INT_PIN, cfg.accelRange);
        
     // last accelerometer report, in joystick units (we report the nudge
     // acceleration via the joystick x & y axes, per the VP convention)
@@ -5442,6 +6101,9 @@
     // start the PWM update polling timer
     polledPwmTimer.start();
     
+    // Timer for configuration change reboots
+    ExtTimer saveConfigRebootTimer;
+    
     // we're all set up - now just loop, processing sensor reports and 
     // host requests
     for (;;)
@@ -5456,7 +6118,7 @@
         LedWizMsg lwm;
         Timer lwt;
         lwt.start();
-        IF_DIAG(int msgCount = 0;)
+        IF_DIAG(int msgCount = 0;) 
         while (js.readLedWizMsg(lwm) && lwt.read_us() < 5000)
         {
             handleInputMsg(lwm, js);
@@ -5472,11 +6134,20 @@
             }
         )
         
+        // process IR input
+        process_IR(cfg, js);
+    
+        // update the PSU2 power sensing status
+        powerStatusUpdate(cfg);
+
         // update flashing LedWiz outputs periodically
         wizPulse();
         
         // update PWM outputs
         pollPwmUpdates();
+        
+        // poll the accelerometer
+        accel.poll();
             
         // collect diagnostic statistics, checkpoint 0
         IF_DIAG(mainLoopIterCheckpt[0] += mainLoopTimer.read_us();)
@@ -5487,7 +6158,7 @@
        
         // collect diagnostic statistics, checkpoint 1
         IF_DIAG(mainLoopIterCheckpt[1] += mainLoopTimer.read_us();)
-
+        
         // check for plunger calibration
         if (calBtn != 0 && !calBtn->read())
         {
@@ -5537,8 +6208,10 @@
             // Otherwise, return to the base state without saving anything.
             // If the button is released before we make it to calibration
             // mode, it simply cancels the attempt.
+            diagLED(1,1,1);
             if (calBtnState == 3 && calBtnTimer.read_us() > 15000000)
             {
+                diagLED(0,0,0);
                 // exit calibration mode
                 calBtnState = 0;
                 plungerReader.setCalMode(false);
@@ -5549,6 +6222,7 @@
             }
             else if (calBtnState != 3)
             {
+                diagLED(0,1,1);
                 // didn't make it to calibration mode - cancel the operation
                 calBtnState = 0;
             }
@@ -5635,10 +6309,12 @@
         bool jsOK = false;
         
         // figure the current status flags for joystick reports
-        uint16_t statusFlags =
-            (cfg.plunger.enabled ? 0x01 : 0x00)
-            | (nightMode ? 0x02 : 0x00)
-            | ((psu2_state & 0x07) << 2);
+        uint16_t statusFlags = 
+            cfg.plunger.enabled             // 0x01
+            | nightMode                     // 0x02
+            | ((psu2_state & 0x07) << 2);   // 0x04 0x08 0x10
+        if (IRLearningMode != 0)
+            statusFlags |= 0x20;
 
         // If it's been long enough since our last USB status report, send
         // the new report.  VP only polls for input in 10ms intervals, so
@@ -5692,7 +6368,7 @@
         
         // If joystick reports are turned off, send a generic status report
         // periodically for the sake of the Windows config tool.
-        if (!cfg.joystickEnabled && jsReportTimer.read_us() > 5000)
+        if (!cfg.joystickEnabled && jsReportTimer.read_us() > 10000UL)
         {
             jsOK = js.updateStatus(statusFlags);
             jsReportTimer.reset();
@@ -5758,8 +6434,23 @@
         }
         
         // if we have a reboot timer pending, check for completion
-        if (rebootTimer.isRunning() && rebootTimer.read_us() > rebootTime_us)
+        if (saveConfigRebootTimer.isRunning() 
+            && saveConfigRebootTimer.read() > saveConfigRebootTime)
             reboot(js);
+            
+        // if a config save is pending, do it now
+        if (saveConfigPending != 0)
+        {
+            // save the configuration
+            saveConfigToFlash();
+            
+            // if desired, reboot after the specified delay
+            if (saveConfigPending == SAVE_CONFIG_AND_REBOOT)
+                saveConfigRebootTimer.start();
+                
+            // the save is no longer pending
+            saveConfigPending = 0;
+        }
         
         // if we're disconnected, initiate a new connection
         if (!connected)
@@ -5809,10 +6500,19 @@
                     diagTimer.reset();
                 }
                 
-                // if the disconnect reboot timeout has expired, reboot
+                // If the disconnect reboot timeout has expired, reboot.
+                // Some PC hosts won't reconnect to a device that's left
+                // plugged in through various events on the PC side, such as 
+                // rebooting Windows, cycling power on the PC, or just a lost
+                // USB connection.  Rebooting the KL25Z seems to be the most
+                // reliable way to get Windows to notice us again after one
+                // of these events and make it reconnect.
                 if (cfg.disconnectRebootTimeout != 0 
                     && reconnTimeoutTimer.read() > cfg.disconnectRebootTimeout)
                     reboot(js, false, 0);
+
+                // update the PSU2 power sensing status
+                powerStatusUpdate(cfg);
             }
             
             // resume the main loop timer