Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

IRRemote/IRReceiver.h

Committer:
arnoz
Date:
2021-10-01
Revision:
116:7a67265d7c19
Parent:
97:fc7727303038

File content as of revision 116:7a67265d7c19:

// IR Remote Receiver
//
// This is a multi-protocol receiver for IR remote control signals.  The
// IR signals are physically received through an external sensor.  Our
// reference device is the TSOP384xx, but most other IR remote sensors
// are similar in design and will proably work.  We have two main requirements
// for the sensor: first, it has to demodulate the IR carrier wave; second,
// it has to present a single-wire digital signal representing the demodulated
// IR status.  We assume active low signaling, where 0V on the signal line 
// represents "IR ON" and Vcc represents "IR OFF".  It would be fairly easy
// to adapt the code to the opposite signaling sense, but I haven't bothered
// to parameterize this because I haven't seen any active-high sensors.  The
// sensor also obviously has to be electrically compatible with the KL25Z,
// which mostly means that it runs on a 3.3V supply.  If your sensor uses
// a different supply voltage, it might still be workable, but you might
// need to interpose a voltage level converter on the logic input to make
// sure that the KL25Z GPIO pin doesn't go above 3.3V, as these pins aren't
// tolerant of higher voltages.
//
// How to wire the sensor
//
// To physically wire the sensor, you just need to connect the sensor's
// Vs and GND pins to the the 3.3V out (P3V3) and GND on the KL25Z,
// respectively, and connect its "OUT" or "data" pin (pin 1 on a TSOP384xx)
// to a free, interrupt-capable GPIO on the KL25Z.  On the KL25Z, all PTAxx
// and PTDxx ports are interrupt-capable (and conversely, PTBxx, PTCxx, and
// PTExx ports aren't, so you can't use one of those).  You should check the
// data sheet for the sensor you're using to see if any other external
// components are required; e.g., the TSOP384xx data sheet recommends a 
// capacitor and resistor for ESD protection and power supply conditioning.
// The data sheet will include a diagram showing the suggested application
// wiring if there are any special considerations like that.  Note that the
// TSOP384xx data sheet doesn't specify exact values for the resistor and 
// capacitor, so I'll mention what I'm using in my test setup: 220 ohms for
// the resistor, 150nF for the capacitor.
// 
// How to use it in your application
//
// To use the receiver in an application, first create an IRReceiver object,
// telling it which pin to use as the sensor input, and how big you want the
// raw sample buffer to be.  The raw buffer needs to be big enough to hold
// samples that arrive during each iteration of your main loop, so you need
// approximately one buffer entry per 250us of your main loop's maximum 
// iteration time.  If RAM isn't tight in your app, just pick a fairly large 
// size (maybe 200 entries); if RAM is tight, figure your worst-case main loop
// time, divide by 250us, and add maybe 25% or 50% padding.  Once you create
// the receiver object, call enable() to enable reception.  You can do this
// once at the outset, or you can selectively enable() and disable() it at
// any time if you only need reception at specific times.  Reception takes
// a small amount of CPU time (in interrupt mode) whenever signals arrive,
// so if you have a time-critical task to do at a time when reception isn't
// useful, you can turn it off to avoid any latency from IR interrupts.
//
//    IRReceiver *rx = new IRReceiver(PTA13, 32);
//    rx->enable();
//
// If you're using the companion transmitter class in the same application
// to create a device that's both an IR transmitter and receiver, you might
// want to tell the receiver about the transmitter, via setTransmitter().
// This causes the receiver to ignore incoming signals whenever the
// transmitter is sending, so that you don't receive your own transmissions.
// This isn't necessary if the receiver is positioned so that it can't see
// the transmitter's signals.
//
//    rx->setTransmitter(tx);
//
// Once you have a receiver set up and enabled, you need to call its process()
// method on each iteration of your main loop.  This method takes all of the
// signals that have been received since the last call and runs them through
// the protocol decoders.  To minimize time spent in interrupt handlers, the
// interrupt handlers merely queue the messages internally; this makes them
// return extremely quickly, so that they don't add any significant latency 
// for other hardware or timer interrupts your application might use.
//
//    rx->process();
//
// Also in your main loop, read incoming IR remote codes by calling
// readCommand() on the receiver.  If a command is available, this will read
// it into an IRCommand object, which tells you the protocol the sender used
// (see IRProtocolID.h), and provides a "universal" representation of the
// command.  The universal representation is protocol-specific mapping of
// the raw data bits to an integer value.  We try to do this in a way that's
// most useful per protocol, with two main goals in mind.  First, if there
// are any internal bits that are more structural than meaningful, such as
// checksums or other integrity checks, we generally remove them.  Second,
// if there are published tables of codes from a manufacturer, we try to
// match the format used there, to make it easier to verify that codes are
// as expected and to make it easier to construct apps around specific types
// of remotes.
//
//    IRCommand cmd;
//    while (rx->readCommand(cmd)) { process the command; }
//
// You can also optionally read the raw incoming data, by calling processOne()
// instead of process().  processOne() runs a reading through the protocol
// decoders but also hands it back to you.  Raw samples are simply "IR ON"
// and "IR OFF" signals with the time the IR was continuously on or off.
// The raw samples are useful if you want to build something like a repeater
// that only has to replicate the physical IR signals without regard to the
// underlying data bits.  Raw signals are obviously also very useful if you
// want to analyze an unknown protocol and figure out how to write a new
// encoder/decoder for it.  One thing that raw signals aren't great for,
// somewhat counterintuitively, is for building a learning remote.  Many of
// the protocols have special ways of handling repeated codes (e.g., when 
// holding down a key) that make verbatim repetition of a signal problematic
// for learning remote use.  If you just repeat a raw code, the receiver
// might be confused into thinking that one key press looks like several
// key presses, or vice versa.  It's better when possible to decode a signal
// into a recognized protocol, store the decoded bit data rather than the
// raw signals as the "learned" code, and then reconstruct the appropriate 
// signal for transmission by re-encoding the learned bit code using the 
// protocol's known structure.
//
//
// Internal architecture
//
// The IRReceiver object is simply a coordinator that manages the sensor
// hardware interface, reads the raw signals from the sensor, and passes
// the raw signals to the protocol objects for decoding.  For each protocol
// we recognize, we define a specialized handler class for the protocol.  
// Each protocol handler implements a state machine for decoding signals 
// using its protocol.  When IRReceiver reads a raw signal, it simply passes 
// it to each of the protocol handlers in turn.  They all operate as 
// independent state machines, so in effect we have specialized receivers 
// for all of the protocols operating in parallel, all eavesdropping on the 
// same incoming stream of signals.  When one of the protocol handlers 
// successfully decodes a complete "command" (a key press on a remote, in 
// most cases), it adds the command to our queue, using a universal 
// representation that we define.  Clients can then read the incoming
// commands from the queue without worrying about the raw signal details.
//
// It might sound chaotic to have all of these different protocol decoders
// working on the same data at the same time, but in practice the various 
// protocols have enough internal structure that only the "right" handler
// will be able to do anything with a given signal, and the rest will just
// ignore it, and bide their time until something shows up that they can make 
// sense of.  It might also sound like a lot of overhead, but in practice 
// it's very lightweight: it takes about 4% CPU to service the decoding 
// process while a signal is actually coming in, and essentially 0% when
// the IR airwaves are silent.  What's more, that 4% CPU time is all in
// application context, not in interrupt context, so it doesn't contribute
// any latency to any other hardware interrupts you need to handle in your
// application.
//
// The individual protocol state machines are all very simple, typically 
// doing just a few integer compares on the incoming timing data per signal.
// They also require very little state, usually on the order of a few 'int's 
// per decoder, which translates to a small RAM footprint.  The decoders 
// operate incrementally and decode in near real time, so decoded commands 
// appear essentially at the same time that their signals finish.
//
// Note that, unlike some other MCU IR libraries, we don't any have sort 
// of global receiver state.  In particular, we don't try to guess about
// message boundaries globally.  All of the boundary detection and protocol 
// state is in the individual protocol decoders.  That eliminates the need
// for heuristics or special cases to guess about what "usually" indicates
// a message boundary across all protocols.  There are enough special cases
// to make such guesses problematic, which becomes apparent if you look at
// the code in libraries that work that way.  Since we don't need to know
// about message boundaries globally, we don't need to make such guesses or
// apply such special cases.  We simply deal in the raw pulses and let 
// each decoder separately judge for itself where its own message boundaries 
// are.  This might seem odd, because the implication is that one decoder
// might think we're in the middle of a message while another decoder 
// thinks we're on a boundary.  But that's just fine, and it's exactly
// why we shouldn't be making those judgments globally: if two protocols
// have contradictory rules like that, the way to reconcile it is to accept
// that there really is no correct global judgment, and leave it to the 
// decoders to track their own states independently.
//
// We receive signals from the sensor via interrupts on the input GPIO pin.
// This allows for the most accurate timing possible, which is important
// because IR coding is entirely in the signal timing.  Interrupts gives us
// much more accurate timing than polling would for obvious reasons.  As
// mentioned above, though, we try to minimize the time we spend in IRQ
// context, since time spent in one interrupt handler translates to added
// latency for any other interrupts that occur at the same time.  To 
// accomplish this, the interrupt handlers don't do any decoding at all.
// They simply add the incoming signal data to an internal queue and then
// return.  We do the decoding work back in application context, by having
// the main loop call our process() routine periodically.  This takes signal
// readings off of the queue and runs them through the decoders.  This
// introduces a small amount of lag time between physically receiving a
// signal and decoding it, but the lag time is only on the order of the
// main loop run time.  In most MCU applications this is a very short
// time, perhaps only microseconds or perhaps as long as a few millseconds.
// But in any case it's almost always so short that a user can't perceive
// the delay, so for all practical purposes decoding is done in real time.
//
//
// How IR remotes work in general
//
// IR remote controls work by transmitting timed pulses of infrared light.
// These pulses are modulated in two ways: first, with a "carrier", which
// is a PWM signal operating at a fixed, relatively high frequency; and
// second, with a lower frequency data signal superimposed on the PWM
// signal.  (And I suppose you could say there's a third layer of 
// modulation in the IR light itself, since that's an electromagnetic 
// wave operating at an even higher frequency of around 300 THz.)
//
// Carrier: The PWM carrier uses a fixed frequency, usually around 40kHz.  
// The carrier doesn't encode any data, since it's just constant fixed-length
// pulses.  Its function, rather, is to provide a regular oscillating signal
// that receivers can use to distinguish data signals from ambient light.
// This is necessary because the IR light wavelengths are also contained
// in sunlight and ordinary household lighting.  (Fluourescent lights even
// have their own characteristic oscillating frequencies in the IR band, so
// the receiver not only has to distinguish the signal from constant
// amgient light levels but also from other types of oscillating light
// levels.  The PWM carrier frequencies used in remotes are chosen based
// on the practical need to distinguish remote control signals from the
// common household interference sources.)  Receivers can separate the 
// an oscillating PWM signal at a particular frequency from other signals
// through a process known as demodulation, which is the same mechanism
// that radio receivers use to pluck AM or FM signals from the jumble of 
// background noise in the radio spectrum.
//
// For our purposes, we don't worry about demodulation in the software,
// since the sensor hardware does that part of the job.  Each type of sensor 
// is designed to demodulate a particular carrier frequency, so you should 
// choose a sensor based on the types of remotes you plan to use it with.  
// Most CE manufacturers have more or less standardized on 38kHz, which is
// why we recommend the TSOP384xx series.  Not everyone is at exactly 38kHz,
// but most are within 2kHz plus or minus, and the TSOP seems to demodulate
// signals within a few kHz of its nominal frequency very well.  38kHz seems
// to be a good centerpoint for home electronics devices, which is why we
// recommend the 38kHz part as a "universal" receiver.  If your application
// only needs to receive from one specific remote (rather than act as a 
// universal receiver), you might be better served with a different TSOP
// part that's tuned to your transmitter's carrier frequency, if that's
// something other than 38kHz.
//
// Data signal: The data signal is superimposed on the PWM carrier by 
// turning the PWM'ed IR source on and off at a lower, variable frequency. 
// These longer on/off pulses are of different lengths.  The data bits are 
// encoded in the varying lengths, although there's no one true way of 
// doing this.  Each protocol has its own way of representing bits as
// combinations of on times and off times, which we'll come to shortly.
//
// "On" pulses are called "marks", and "off" pulses are called "spaces".  
// The terms come from wired asynchronous protocols, which share many 
// properties with IR signals at this level.
//
// Note that each pulse has to be long enough to contain some minimum 
// number (maybe 5-10) of PWM pulses, because otherwise the demodulator 
// wouldn't be able to detect the presence or absence of the underlying 
// PWM pulses.  This makes IR remote codes fairly slow in terms of data 
// rate, since the absolute minimum time per bit is the time in the shortest 
// data pulse.  Most codings actually use at least two pulses per bit for 
// the sake of signal integrity, so the effective data rate lower still.  
// Fortunately, this is all rather unimportant, since IR remotes don't 
// need a very high data rate.  They're mostly used to transmit button 
// presses made by hand by a human user, which are at a fairly low rate
// to start with; plus, the amount of data per button is minuscule, 
// usually from 8 to 32 bits.
//
// Encodings
//
// The timing of the marks and spaces carries the information, but exactly
// how it does this is a whole separate matter, known as an encoding.  An
// encoding is a mapping from '0' and '1' bits to a pattern of marks and
// spaces, and vice versa.  At first glance, it might seem that you could
// just use a mark as a '1' and a space as a '0', and in fact some protocols
// do something like this.  But that simple approach has some big drawbacks
// arising from the lack of a shared clock between sender and receiver.
// Most encodings therefore do something to embed a timing signal of some
// sort within the data signal, by using the lengths of the pulses to encode 
// bits rather than just the presence of the pulses.
//
// There are probably an infinite number of possible ways to do this in
// principle.  Fortunately, the CE companies have only put a finite number
// of them into practice.  In fact, it seems that we can cover practically
// all of the remotes out there by considering a small handful of encoding 
// schemes.  Here are the main ones, and the ones we can use in this
// receiver library:
//
//  - Async bit stream.  This is basically the IR equivalent of a wired
//    UART.  Each code word consists of a fixed number of bits.  Each bit
//    is represented by IR ON for '1' and IR OFF for '0', transmitted for
//    a fixed time length.  To transmit, simply turn the IR on and off in
//    sequence for the fixed bit time per bit.  To receive and decode,
//    observe whether the IR signal is on or off in each time window. 
//    This type of protocol looks simple, but it presents some difficulties
//    in implementation, because it doesn't provide any cues embedded in
//    the IR signal to help the receiver synchronize with the sender or
//    recognize the boundaries of code words, as all of the other common
//    protocols do.  That might be why this class seems to be rarely used 
//    in real applications.  Protocols based on simple async bits usually
//    add something at the protocol level that helps the reciever detect
//    word boundaries and check signal integrity.
//
//  - Pulse distance coding, also known as space length coding.  In this 
//    scheme, marks are all of equal length, but spaces come in two lengths, 
//    A and B, where B is much longer than A (usually twice as long, but it
//    could be even longer).  A encodes 0 and B encodes 1.  The marks serve
//    as regular clock signals, allowing the receiver to keep in sync with
//    the sender, and the long and short space times (A and B) are different
//    enough that the receiver can easily distinguish them reliably, even
//    with a low-precision clock.  This scheme is probably the most widely
//    used in CE products, because it's the encoding used by the NEC 
//    protocol, which most Japanese CE companies use.
//
//  - Pulse length coding, also known as mark length coding.  This is simply 
//    the inverse of pulse distance coding: spaces are all of equal length, 
//    and marks come in two lengths, with the short mark encoding 0 and the 
//    long mark encoding 1.  This is practically the same in all meaningful
//    ways as the space length coding; the only reason both kinds exist is
//    probably that either someone had a bad case of NIH or they wanted to
//    avoid paying a patent royalty.  Mark length coding is the scheme Sony
//    uses (in their SIRCS protocol).
//
//  - Manchester coding.  The message is divided into time slices of 
//    equal size, one bit per slice.  Within each slice, the IR is on for 
//    half the window and off for half the window.  The 0 and 1 bits are
//    encoded by the direction of the transition: if a bit window starts
//    with a mark (IR ON) and ends with a space (IR OFF), it's a '1'; if it
//    starts with a space and ends with a mark, it's a '0'.  Or vice versa.
//    Each mark or space therefore lasts for either 1/2 or 1 bit time 
//    length, never longer.  This makes it fairly easy for the receiver to 
//    distinguish the two time lengths, even with a fairly low-precision 
//    clock, since they're so different.  It's also easy for the receiver
//    to distinguish each bit, since there's always at least one transition
//    (mark to space or space to mark) per bit.  What's more, '0' and '1' 
//    bits take the same time to transmit (unlike the mark-length and 
//    space-length protocols), so every code word (assuming a fixed bit 
//    count) takes the same time regardless of the bit values within. 
//    Manchester modulation is used in the Philips RC5 and RC6 protocols, 
//    which are widely used among European CE companies.
//
// Protocols
//
// On top of the encoding scheme, there's another level of structure called
// a protocol.  A given protocol uses a given encoding for the data bits,
// but then also adds some extra structure.  
//
// For starters, the IR protocols all work in terms of "code words".  In 
// computer terms, a code word amounts to a datatype with a fixed number 
// of bits.  For example, the NEC protocol uses a 32-bit code word: each 
// button press is represented by a 32-bit transmission.  A single key 
// press usually maps to a single code word, although not always; the 
// Pioneer protocol, for example, transmits two words for certain buttons, 
// using a special "shift" code for the first word to give a second meaning 
// to the second word, to extend the possible number of commands that would
// be otherwise limited by the number of bits in a single code word.

// Second, most of the IR protocols add special non-data signals that
// mark the beginning and/or end of a code word.  These are usually just
// extra-long marks or spaces, which are distinguishable from the marks
// and spaces within a code word because they're too long to be valid in
// the data encoding scheme.  These are important to reliable communication
// because the sender and receiver don't have any other way to share state 
// with each other.  Consider what would happen if someone walked in the
// way while you were transmitting a remote code: the receiver would miss
// at least a few data bits, so it would be out of sync with the sender.
// If there weren't some way to distinguish the start of a code word from
// the IR pulses themselves, the receiver would now be permanently out
// of sync from the sender by however many bits it missed.  But with the
// special "header" code, the receiver can sync up again as soon as the
// next code word starts, since it can tell from the timing that the
// header can't possibly be a bit in the middle of a code word.


#ifndef _IRRECEIVER_H_
#define _IRRECEIVER_H_

#include <mbed.h>

#include "IRRemote.h"
#include "IRCommand.h"
#include "circbuf.h"
#include "FastInterruptIn.h"


// IR receiver protocol interface.  This contains functions that we only
// want to make accessible to the protocol decoders.
class IRRecvProIfc
{
public:
    // write a command to the command queue
    void writeCommand(IRCommand &cmd) { commands.write(cmd); }

protected:
    // Decoded command queue.  The protocol handlers add commands here
    // as soon as they decode them.
    CircBuf<IRCommand, 8> commands;
};


// IR Remote Receiver
class IRReceiver : protected IRRecvProIfc
{
public:
    // Construct a receiver with the given data input pin.  The receiver
    // is initially disabled.  To start receiving signals, call enable().
    //
    // Choose a raw buffer size according to the longest iteration time
    // for your main application loop between the required periodic calls 
    // to our process() function.  The interrupt handlers write pulse times
    // to the raw buffer as the pulses arrive, and these are held in the 
    // buffer until they're removed by process().  The raw buffer only needs
    // to be big enough for the "backlog" that occurs between the real-time
    // incoming signals and the main loop's processing calls.  The fastest
    // IR pulses are about 250us long, so size the buffer according to how
    // many 250us intervals will occur in the worst case, that is, the 
    // longest main loop iteration.  If the main loop always runs in 2.5ms 
    // or shorter, that means you need about a 10-element buffer.  To be
    // conservative, size it at perhaps 2x the expected maximum.
    //
    IRReceiver(PinName rxpin, size_t rawBufCount);
    
    // Destructor
    ~IRReceiver();
    
    // Optionally connect to a transmitter, to suppress reception while
    // we're transmitting.  This prevents spuriously receiving our own
    // transmissions, if our IR LED and sensor are physically close enough
    // to one another that our sensor would pick up light from our LED.
    // If the two are physically isolated so that we can't receive our
    // own transmissions, it's not necessary to connect the transmitter
    // here, as there's no restriction on the software side on sending
    // and receiving simultaneously - the suppression is only needed to
    // avoid self-interference with the physical IR signals.
    void setTransmitter(class IRTransmitter *transmitter)
    {
        this->transmitter = transmitter;
    }

    // Enable/disable our interrupt handlers.  If the main program
    // doesn't need IR input, it can disable the receiver so that
    // it doesn't consume any CPU time handling interrupts.
    void enable();
    void disable();
    
    // Read a command.  Returns true if a command was available, filling
    // in 'cmd'.  Returns false (without blocking) if no commands are 
    // available.
    bool readCommand(IRCommand &cmd) { return commands.read(cmd); }
    
    // Is a command ready to read?
    bool isCommandReady() { return commands.readReady(); }
    
    // Process signals received.  The application main loop must call this
    // as frequently as possible to process incoming signals from the raw
    // buffer.  This processes all samples in the raw buffer before
    // returning.
    void process();
    
    // Process and retrieve one raw pulse.  The application main loop can
    // optionally call this, instead of process(), if it wants to retrieve
    // each raw sample for its own purposes in addition to running them 
    // through the protocol state machines.  If no sample is available, we
    // immediately return false - the routine doesn't block waiting for a
    // sample.  If a sample is available, we fill in 'sample' with the pulse 
    // time in microseconds, and set 'mark' to true if the sample was a mark, 
    // false if it's a space.  
    //
    // To use this instead of process(), on each main loop iteration, call 
    // this function in an inner loop until it returns false.  That'll ensure 
    // that all pending samples have been processed through the protocol 
    // state machines and that maximum buffer space is available for the next 
    // main loop iteration.
    bool processOne(uint32_t &sample, bool &mark);
    
    // Process and retrieve one raw pulse.  This works the same as the
    // two-argument version above, but returns the sample in our internal
    // format: the sample value is a time reading in 2us units, and the low
    // bit is 1 for a mark, 0 for a space.  To convert to a time reading in
    // microseconds, mask out the low bit and multiply by 2.
    bool processOne(uint16_t &sample);

    // Maximum pulse length in microseconds.  Anything longer will simply 
    // be represented with this value.  This is long enough that anything
    // longer has equivalent meaning in any of our protocols.  Generally,
    // space longer than this will only occur in a silent interval between
    // transmissions (that is, while no one is sending any codes), and a
    // mark longer than this could only be interference or noise.
    //
    // This value should be chosen so that it's high enough to be readily
    // distinguishable (in terms of our error tolerance) from the longest
    // *meaningful* space or pulse in any protocol we need to handle, but
    // not much higher than that.  It shouldn't be too long because it has
    // a role as an inactivity timeout on receive: we can't always know
    // that a signal has ended until there's inactivity for this amount
    // of time.  If the timeout is too long, it can become noticable as
    // lag time in recognizing signals.  In practice, the longest gap time
    // between repeating signals in commonly used protocols is in the 
    // neighboorhood of 100ms.
    //
    // This value is chosen to be the largest we can fit into a 16-bit
    // int, taking into account our 2X scaling and our use of the low bit
    // for a mark/space indicator.  That leaves us with 14 bits and 2X scale.
    static const uint32_t MAX_PULSE = 131068;    

private:
    // Input pin.  Reads from a TSOP384xx or similar sensor.  Any
    // sensor should work that demodulates the carrier wave and 
    // gives us an active-low input on the pin.  
    //
    // Note that we use our FastInterruptIn replacement instead of the
    // mbed InterruptIn.  We don't actually need the higher speed here of
    // FastInterruptIn, but we have to use it anyway because other parts
    // of the system use it.  The two classes don't play nice together:
    // the whole app has to use one or the other.
    FastInterruptIn pin;

    // IR raw data buffer.  The interrupt handlers store the pulse
    // timings here as they arrive, and the process() routine reads from
    // the buffer.
    //
    // Samples here are limited to 16 bits, so the longest time that
    // can be represented is 65535us.  Anything longer is capped to this.
    //
    // To keep track of marks vs spaces, we set the low-order bit of
    // each sample time to 1 for a mark and 0 for a space.  That means
    // that the times are only good to 2us precision, but that's plenty
    // of precision for all of the IR protocols, since the shortest time 
    // bases are around 250us.
    CircBufV<uint16_t> rawbuf;
    
    // Pulse timer.  We reset the timer at the start of each pulse, so
    // it tells us the duration thus far of the current pulse at any 
    // given time.  We stop the timer (without resetting) any time a
    // pulse reaches the maximum length, to ensure that the timer never
    // rolls over, even in the indefinite gap between codes.
    Timer pulseTimer;
    
    // flag: the pulse timer has reached IR_MAX_PULSE
    bool pulseAtMax;
    
    // current pulse state: mark = 1, space = 0
    bool pulseState;
    
    // start the pulse timers with the new pulse state (1=mark, 0=space)
    void startPulse(bool newPulseState);
    
    // end the current pulse, checking that the pulse state matches the
    // current state
    void endPulse(bool lastPulseState);

    // process a pulse through our protocol handlers
    void processProtocols(uint32_t t, bool mark);
    
    // rise and fall interrupt handlers for the input pin
    static void cbFall(void *obj) { ((IRReceiver *)obj)->fall(); }
    static void cbRise(void *obj) { ((IRReceiver *)obj)->rise(); }
    void fall();
    void rise();
    
    // timeout for time-limited states
    Timeout timeout;
    
    // timeout handler for a pulse (mark or space)
    void pulseTimeout(void);
    
    // Connected transmitter.  If this is set, we'll suppress reception
    // while the transmitter is sending a signal, to avoid receiving our
    // own transmissions.
    class IRTransmitter *transmitter;
};

#endif