Note: staff-provided content does not represent an official statement from FARGOS Development, LLC. The policy on staff-authored content can be found here.


Go back to Geoff's home page.

An Arduino-based Train Controller with Sound Effects

I have several Nanoblock Nanogauge models of various trains that I had used during travel in Japan. Since these models can be upgraded with motors and trucks to enable their operation on standard N-gauge model rail track, I wanted to make a more active display rather than just having them sit on a shelf. I envisioned a back-and-forth trolley-like movement that would have a train stop at an station platform and play some relevant audio announcements.

Note: The control circuitry of this design will work with any conventional 2-rail direct current (DC) model train that uses between 5 and 35 volts DC. Trains based on 3-rail alternating current (AC), such as classic Lionel O-gauge and Märklin, as well as digitally-controlled engines, are not supported.

Modules

The system is broken into several physical modules tied together with some wiring and transistors.

The Arduino sketch implementing the control logic is found at the bottom of this document.

Arduino Nano Microcontroller

The Arduino Nano microcontroller is a small (45mm x 18mm) board that has approximately 30 Kbytes of flash memory available for program instructions and static data and approximately 2 Kbytes of RAM for runtime variables and stack. While these constraints are tight, they are still sufficiently generous to enable the functionality needed. A more complicated configuration, including an OLED display, could be handled by an Arduino Mega (which could support up to 15 tracks) or an Arduino Due.

Arduino Nano Board
Arduino Nano Board

L298N H-Bridge

L298N H-Bridge Dual Motor Driver
L298N H-Bridge Dual Motor Driver Board

Trains are moved back and forth using direct current whose polarity and voltage are controlled via L298N motor drivers. This choice does preclude the use of alternating current train engines or those controlled via digital signals. One L298N H-bridge module can control one or two train tracks. These modules support up to 35 VDC and are controllable using pulse width modulation. Direction of the train is selected via two digital pins, which suggests that 3 pins per track, one of which is PWM-capable, are required; however, this resource can be reduced to one PWM-capable pin and one digital pin per track. Since the software drops the PWM value to 0 whenever a train is stopped, the direction indicator can be generated by creating a logical not signal from the remaining control pin and using that for the second directional input signal to the respective L298N port.

DFPlayer Mini

DFPlayer Mini Board
DFPlayer Mini Board

When moving beyond simple tones that could be generated using a square wave, sound data is relatively bulky and complex. Arbitrary sound data of varying and unknown length is infeasble to store within the constraints of the 30 Kbytes of flash memory. Storing the data in a standard flash memory card would be ideal but the complexity of understanding the FAT filesystem (or a variant), reading the data, decoding the audio file format (such as MP3 or WAV) and turning it into an audio signal would also be prohibitively expensive. Fortunately, the DFPlayer Mini is a remarkable piece of hardware that does everything needed. The DFPlayer Mini board is able to accept a rich suite of commands that are sent from the Arduino board via a serial link. The Arduino Nano uses 5 volt DC signal levels and the DFPlayer Mini is a 3.3 volt DC device, so a 1000 ohm resistor is used on the transmit line from the Arduino to reduce the voltage level. Because one has no knowledge of how long a particular sound file will play, the DFPlayer Mini provides an output pin that indicates whether or not the device is busy and this is connected to the Arduino Nano to provide feedback that a command has begun being processed and when a sound has completed.

There are many imperfect clones on the market that have taken some shortcuts and do not support the complete documented command set; the software made available below makes use of only the reduced set of available command directives.

Caution: in practice, these devices are very sensitive to voltage and there are lots of reports of failures to be found on the web. They really want to be driven at 3.3 VDC and can tolerate up to a maximum of 5 VDC. With one exception, where I accidentally applied 2 amps worth of 12 VDC to a component rated for 5 VDC, these are the only components I have had fail and I have burned (*) through more of these than I care to count. The design below includes a trivial over-voltage protection circuit using a Zener diode. The implementor is cautioned to not overlook this minimal level of protection; feel free to do better, but do make provision for limiting the voltage that can be seen by this device.

* "burned" is the accurate verb.

PAM8403 Audio Amplifier

PAM8403 Two Channel Amplifier
PAM8403 Two Channel Amplifier

The DFPlayer Mini does generate an amplified audio signal suitable for driving a monaural headphone; however, this is insufficient for our purposes, so the digital-to-analog output from the DFPlayer Mini is sent to a PAM8403 dual-channel audio amplifier that is connected a pair of stereo speakers. The amplifier module also includes a rotary dial for volume control that allows the end user to control the final output volume.

Discrete Through-the-Hole Components

The approximate position of a train on its respective track is determined using 3144 Hall effect sensors hidden under the track that detect a magnetic field. At least one neodymium magnet is affixed to the bottom of the train to trigger the Hall effect sensors. It is possible to use more than one magnet to mark both the beginning and end of the train consist, but this is not a requirement.

Note: a magnet's polarity (north/south) is important and each magnet should be tested against the Hall effect sensors prior to being affixed to the train bottom. The magnet will not be detected if it is mounted upside down.

The 3144 Hall effect sensors are digital sensors that normally output a high signal and output a low signal when the presence of a strong enough magnetic field is detected. There are analog versions of Hall effect sensors that are capable of indicating relative field strength, but this design uses a digital all-or-nothing approach.

Each track has at least 3 sensors, one to mark a station platform in the middle and the others to denote the two track ends. As noted previously, the software provided for the Arduino Nano V3 supports the use of more than 3 sensors per track and more than 2 tracks. The Arduino Nano hardware is constrained by available input pins, but an Arduino Mega or Due have sufficient pins to handle more than 3 sensors per track and more than 2 tracks. Alternatively, a shift register could be used to multiplex multiple sensors (e.g., a 74HC166 can handle 8 sensors per digital input pin), but this feature is not provided in version 1 of the application software.

Each distinct sensor circuit consists of a minimum of configuration of a 3144 Hall effect sensor and a 10K ohm pull-up resistor.

It is completely optional, but the circuit design below does make provision for driving an LED that indicates when a train is at the respective end of a track or the station in the middle. Because the 3144 Hall effect sensors go low (are grounded) when the presence of the magnetic field is detected and the LEDs need the opposite signal to turn on, a 2N3906 PNP transistor is used to invert the 3144 sensor output and control the power for an LED.

A 5K ohm current limiting resistor is used for the transistor base and a 320 ohm current limiting resistor is used for the indicator LED. If needed, higher ohm values may be used for the pull-up resistor for the Hall effect sensor; feasible values are in the range 10K-150K ohms, with lower values proving more flexibility.

Red LEDs are selected for indicator lights to minimize the forward voltage needed to illuminate them. The suggested LEDs are 3 millimeters in diameter (chosen for their small size) and are rated 2.2 V(F) and 20 milliamps I(F). The LEDs are mounted on a separate board and plugged into the LED lamp holders.

Duration or speed of a train movement cycle is adjustable via a rotary potentiometer. It is driven by the 3.3V regulated output provided by the Arduino board. The 3.3V regulated output is tied to both the Arduino AREF signal, which sets the levels for the analog-to-digital conversions, as well the potentiometer. Using a #4 spade connector with the forks slightly squished inwards is a convenient way to tie the AREF and 3.3V pins together via single wire that is then connected to one of the leads on the potentiometer. To help reduce noise/jitter in the output level, a 22 nF capacitor and 10K ohm resistor in series are tied between the AREF and ground on the potentiometer.

Human Presence Detector

RCWL-0516 Microwave RADAR
RCWL0516 Microwave RADAR Doppler Detector

An RCWL-0516 Doppler RADAR sensor is used to detect motion in the area. Unlike a Passive Infrared (PIR) sensor, the RADAR device can be hidden behind a wood panel as it does not need to be visible.

The RCWL-0516 board generates an output signal that goes high for a couple of seconds when motion is detected, which is the opposite of many passive infrared receivers. A 2N3904 NPN transistor is used to invert the signal so that a normally high signal is presented and turns low when a human presence is detected. This permits both substitution of a common Passive Infrared sensor board and also allows manual triggering to be introduced using a single pole push button.

The design below includes a toggle switch to enable/disable use of the detector as well as a single pole push button manual trigger as an element of the signal circuit. Normally this button circuit will be open, but pressing the button will close the circuit and connect it to ground. For safety, the signal is grounded through a current limiting resistor of 1K ohms. The pull-up resistor built into the Arduino processor is used to prevent the signal from randomly floating when the button is not pressed.

Power Supplies

LM2596 Buck Converter
LM2596 Buck Converter

Power is obtained from the household mains using a 2-amp 12-volt DC transformer. The always-on 12-volt DC power from the transformer is connected to two single-pole single-throw switches. One optional switch is used to control power to an LED light strip (used for indirect lighting) and the other controls the power fed to the Arduino Nano, L298N motor controller and an LM2596 buck converter that steps the voltage down to 5 volts.

Note: the output voltage of the LM2596 voltage converter is controlled by a trim potentiometer and needs to be dialed in to obtain the desired 5 volts DC.

Circuit Schematics

The schematic for the Train Controller is broken across 2 sheets and is implemented using several boards and distinct components.

Main circuit

Train Controller Schematic Sheet 1
Arduino Nano Module Interconnects

Sensor and LED interconnect Board

Sensor and LED Interconnect Board
Sensor and LED Interconnect Board

The 3144 Hall effect sensors and LED presence indicators are connected via an intermediate circuit board. JST-style connectors are used to connect the cabling between boards.

Train Controller Schematic Sheet 2
Hall effect sensors and LED indicators
Front of LED Indicator Board
Front of LED indicator board and rear of panel
Rear of LED Indicator Board
Rear of LED indicator board

Arduino Pin Assignments

Digital Pins

Digital Pins
Pin Name Secondary Function Description Used For
D0 Digital Pin 0 RX Receive pin for Serial UART Debugging console
D1 Digital Pin 1 TX Transmit pin for Serial UART Debugging console
D2 Digital Pin 2 INT0 Interrupt Pin 0 Sound busy
D3 Digital Pin 3 INT1, PWM Interrupt Pin 1 Human presence detected
D4 Digital Pin 4 Track 1 direction A
D5 Digital Pin 5 PWM Track 1 direction B
D6 Digital Pin 6 PWM Track 1 speed
D7 Digital Pin 7 Track 2 direction A
D8 Digital Pin 8 Track 2 direction B
D9 Digital Pin 9 PWM Track 2 speed
D10 Digital Pin 10 SS,PWM SPI Slave Select Pin Track 1 end A Hall effect sensor
D11 Digital Pin 11 MOSI, PWM SPI Master Out-Slave In
D12 Digital Pin 12 MISO SPI Master In-Slave Out Control of MP3 decoder RX
D13 Digital Pin 13 SCK SPI Clock, LED Control of MP3 decoder TX

Analog Pins

Analog Pins
Pin Name Secondary Function Description Used For
A0 Analog Pin 0 Use for reading potentiometer value
A1 Analog Pin 1 Track 1 station Hall effect sensor
A2 Analog Pin 2 Track 1 end B Hall effect sensor
A3 Analog Pin 3 Track 2 station Hall effect sensor
A4 Analog Pin 4 SDA I2C Data Out OLED display (optional)
A5 Analog Pin 5 SCL I2C Clock OLED display (optional)
A6 Analog Pin 6 Track 2 end A Hall effect sensor
A7 Analog Pin 7 Track 2 end B Hall effect sensor

Power Pins

Power Pins
Pin Name Notes
5V 5V (Regulated) Source
3.3V 3.3V Source Used as analog reference level, tied to AREF
GND Ground
RESET Reset
Vin DC Jack Input Voltage
IOREF I/O Reference Voltage. This pin is connected to 5V for the UNO
AREF ADC Reference Voltage Insert other voltage (0-5V only) to use as reference for analog conversions

Train Controller Source Code

The source code to the Train Controller application that runs on the Arduino is illustrated below. The most current release can be retrieved from this TrainController source download link. The .ino files are really C++ source with an alternate file suffix to permit association with the Arduino IDE application.

The comments within the source code provide more detail that will not be repeated here.

/*! \file Multi-track back-and-forth train and sound effect
 * controller using an Arduino Nano V3.0 (or better).
 *
 * Train location is detected using 3144 Hall effect sensors.
 * Engine speed and direction is controlled via dual L298N motor interface.
 * Sounds are played using a DFPlayer Mini.
 *
 * \note The Arduino Nano is an 8-bit processor with 32K of flash memory
 * and 2K of RAM; consequently it has a 16-bit address space
 * (i.e., sizeof(void *) is 2).
 * The data structures implemented take this into account with respect
 * to alignment of member variables, but will work on 32-bit and 64-bit
 * address spaces, albeit with some additional padding inserted between
 * member variables.
 *
 * Arduino applications have two main entry points:  a one-time
 * invocation of setup() followed by never-ending calls to loop()
 * at approximately 5 millisecond intervals.
 *
 * This application is realized via several classes:
 *   - ClockWithRollover handles potential clock rollover
 *   - BlinkLEDinSequence provides services to flash the onboard LED
 *     in specified patterns.
 *   - templatized AnalogReadingSmoother is used to filter out jitter from
 *     the values provide by the analog-to-digital converters.
 *   - MotionTriggerState tracks the signal changes created by a
 *     proximity detector or manual button pressing.
 *   - TrackSoundState maintains the state as to what sound should
 *     next be played.  One per sound type per track.  Currently 2 are
 *     used per track, one for station arrival and one for ambient
 *     between stations sounds.
 *   - templatized TrackSensorStates maintains the state of the
 *     Hall effect sensors associated with a specific track.
 *   - TrainSoundController interfaces with a DF Mini Player board
 *     and maintains a queue of sounds to be played.  One global
 *     object that handles the contention of mutiple pending requests.
 *   - SimpleL298N provides an interface to L298N motor drivers.  One
 *     per track.
 *   - templatized TrainState collects all of the track-specific data,
 *     such as TrackSensorStates, SimpleL298N, and TrackSoundState.
 *
 * \author Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html
 * \date 2022/03/23 Initial public release
 */

// -----------------------------------------------------------------------
// Optional debug output settings
// -----------------------------------------------------------------------

/*! \brief Enables debug output to serial console.
 *
 * Normally 0, but can be set to nonzero to enable debug output to
 * the serial console. 2 also outputs the file name instead of just the
 * line number.
 * \note This must be nonzero to enable any debug output.  All debug
 * output is inhibited if set to 0.
 * \sa DEBUG_ANALOG_CONVERSION, DEBUG_MOTION_SENSOR,
 * DEBUG_SOUND_CONTROLLER, DEBUG_TRACK_POWER, DEBUG_TRACK_SENSORS
 */
#define LOG_ENABLED 1

/*! \brief Console baud rate
 *
 * This value should match the baud rate selected in the Arduino IDE's
 * serial monitor window.
 */
#define CONSOLE_BAUD_RATE 57600

/*! \brief Debug analog-to-digital conversion changes
 *
 * Normally 0, but can be set to nonzero to enable debug output
 * for changes in the potentiometer resistance.
 */
#define DEBUG_ANALOG_CONVERSION 0

/*! \brief Debug motion interrupt
 *
 * Normally 0, but can be set to nonzero to enable debug output
 * for motion detection sensor.
 */
#define DEBUG_MOTION_SENSOR 1

/*! \brief Debug sound controller
 *
 * Normally 0, but can be set to nonzero to enable debug output
 * for sound controller. 2 provides noisy detail.
 */
#define DEBUG_SOUND_CONTROLLER 1

/*! \brief Debug track power
 *
 * Normally 0, but can be set to nonzero to enable debug output
 * for motor power changes. 2 provides noisy detail on event queue.
 */
#define DEBUG_TRACK_POWER 0

/*! \brief Debug track presence sensors
 *
 * Normally 0, but can be set to nonzero to enable debug output
 * for track Hall effect sensors changes. 2 provides more detail
 * and 3 provides noisy detail.
 */
#define DEBUG_TRACK_SENSORS 2

/*! \brief Enable optional OLED display.
 *
 * Set to 1 if optional SSD1306 controller attached via I2C.
 *
 * \note Not feasible on systems with only 32K of Flash memory.
 */
#define OLED_ENABLED 0

/*! \brief Enable reading serial console to process debug commands.
 *
 * Normally 0, but can be set to non-zero to
 * enable entering debug commands.
 */
#define ENABLE_CONSOLE_COMMANDS 0

/*! \brief Enable using LED blinking to display error conditions.
 *
 * If no console is attached (as would normally be the case),
 * the onboard LED of the Arduino board can be blinked in sequence
 * to communicate an error status.
 */
#define ENABLE_LED_BLINK_NOTIFICATIONS 1

/* NOTE: the station synchronization options are track-specific
 * and do not scale with MAX_TRAIN_TRACKS.
 */
/*! Keep station sounds synchronized on track 1. 
 */
#define SYNCHRONIZE_STATION_WITH_AMBIENT_TRACK1 1

/*! Keep station sounds synchronized on track 2. 
 */
#define SYNCHRONIZE_STATION_WITH_AMBIENT_TRACK2 1

/*! \brief Enable period dumping of state to the Serial console.
 *
 * The number of seconds between calls to the dumpState() routine
 * is specified.  A value of 0 indicates no periodic output is desired and
 * is the usual setting.
 */
#define DUMP_STATE_INTERVAL_SECONDS 0

/*! \brief Enable test cycle of motor speeds.
 *
 * Normally defined as 0 to disable the test cycles.
 */
#define TEST_MOTOR_SPEED 0

// -----------------------------------------------------------------------
// Hardware-related settings, including pin assignments
// -----------------------------------------------------------------------
/*! max aount of time for request to be processed in milliseconds */
#define MAX_AUDIO_TIMEOUT 500

/*! \brief Max amount of time for acknowlegement of command in milliseconds */
#define MAX_AUDIO_ACK_TIMEOUT 200

/*! \brief Pin used for analog-to-digital conversion of potentiometer reading */
#define CYCLE_DURATION_ANALOG_PIN A0

#define PIN_TRACK1_END_A_SENSOR 10
#define PIN_TRACK1_END_B_SENSOR A2 /* swap these */
#define PIN_TRACK1_STATION_SENSOR A1

#define PIN_TRACK2_STATION_SENSOR A3
// A4=I2C Data Out, A5=I2C Clock
#define PIN_TRACK2_END_A_SENSOR A6
#define PIN_TRACK2_END_B_SENSOR A7

#define PIN_SERIAL_RECEIVE 0 /* console receive pin, for debugging */
#define PIN_SERIAL_TRANSMIT 1 /* console transmit pin, for debugging */

#define PIN_INTERRUPT0 2 /*!< used for audio controller busy signal */
#define PIN_INTERRUPT1 3 /*!< used for motion detection/manual trigger */

#define PIN_TRACK1_DIR0 5
#define PIN_TRACK1_DIR1 4
#define PIN_TRACK1_MOTOR_SPEED 6 /* pin must be PWM-capable */

#define PIN_TRACK2_DIR0 8
#define PIN_TRACK2_DIR1 7
#define PIN_TRACK2_MOTOR_SPEED 9 /* pin must be PWM-capable */


/*! \brief Pin used to receive responses from DF Mini Player board */
#define PIN_AUDIO_CONTROLLER_RX 11
/*! \brief Pin used to send commands to DF Mini Player board */
#define PIN_AUDIO_CONTROLLER_TX 12

// -----------------------------------------------------------------------
// Local track configuration
// -----------------------------------------------------------------------

/*! \brief Defines number of tracks to be controlled.
 *
 * \note An Arduino Nano can handle a maximum of 2 tracks.
 */
#define MAX_TRAIN_TRACKS 1

/*! \brief Defines number of Hall effect sensors per track.
 *
 * Typically 3.
 */
#define MAX_SENSORS_PER_TRACK 3

/*! \brief Enable use of analog dial for controlling max speed of train.
 *
 * If set to 1, the analog dial will be used to control the speed of the
 * train and the maximum cycle time will fixed as
 * be set to MAX_OPERATION_TIME_IN_MINUTES.  If set to 0, the maximum
 * train speed with be set to TrainState::FASTEST_PWM_SPEED and
 * the duration of cycles will be derived from the analog dial.
 */
#define ANALOG_DIAL_CONTROLS_SPEED 1

/*! \brief Maxium runtime for a complete set of back-and-forth cycles,
 * either triggered by a motion sensor or a manual button press.
 * Used to scale reading from potentiometer if ANALOG_DIAL_CONTROLS_SPEED
 * was defined as 0.
 *
 * \sa MAX_MOTOR_RUNTIME_MS
 */
#define MAX_OPERATION_TIME_IN_MINUTES 30

/*! \brief Maximum amount of time to allow a train motor to be on.
 *
 * This setting protects against missing a sensor change and needs to be
 * long enough to permit a slow-moving train to travel past a Hall effect
 * sensor.
 *
 * Expected to be less-than-or-equal to MAX_OPERATION_TIME_IN_MINUTES.
 */
#define MAX_MOTOR_RUNTIME_MS (static_cast<int32_t>(15) * 1000)

/*! \brief Duration of full speed to stop in milliseconds
 */
#define SLOW_DOWN_INTERVAL_MS 500

/*! \brief Duration of stop to full speed in milliseconds
 */
#define SPEED_UP_INTERVAL_MS 10000

/*! \brief Minimum amount of time to delay at end-of-track stations
 */
#define MIN_END_STATION_WAIT_TIME_MS 5000

/*! \brief Maximum amount of time to delay at end-of-track stations
 */
#define MAX_END_STATION_WAIT_TIME_MS 45000

/*! \brief Minimum amount of time to delay at mid-point station
 */
#define MIN_MID_STATION_WAIT_TIME_MS 10000
/*! \brief Maximum amount of time to delay at mid-point station
 */
#define MAX_MID_STATION_WAIT_TIME_MS 30000

/*! \brief Expected maximum amount of time for a sound.
 */
#define MAX_SOUND_LENGTH_SECONDS 360


// these can be changed if a different interface is used
#define SOUND_ACTIVE LOW /*!< Sound card's BUSY signal is low when active */
#define SOUND_IDLE HIGH /*!< Sound card's BUSY signal is high when idle */

/*! Minimum amount of time between activation cycles */
#define MIN_SECONDS_BETWEEN_CYCLES 10

// -----------------------------------------------------------------------
// Sound file configuration
// These parameters are completely dependent upon the content
// loaded on the SD card placed into the DF Mini Player board.
// See definitions of trackSounds[] and ambientSounds[]
// -----------------------------------------------------------------------

/*! Currently, sounds for 30 stations on the Yamanote Line */
#define YAMANOTE_LINE_STATION_TOTAL 30
#define YAMANOTE_LINE_SOUNDS_PER_STATION 4 /*!< Up to 4 sounds per station */
#define YAMANOTE_LINE_FOLDER_NUMBER 1
#define YAMANOTE_LINE_BASE_SOUND 1

/*! Currently, sounds for 30 stations on the Yamanote Line */
#define YAMANOTE_AMBIENT_STATION_TOTAL 30
#define YAMANOTE_AMBIENT_SOUNDS_PER_STATION 1
#define YAMANOTE_AMBIENT_FOLDER_NUMBER 3
#define YAMANOTE_AMBIENT_BASE_SOUND 1

#define SHINKANSEN_STATION_TOTAL 1 /*!< Currently, just Kobe */
#define SHINKANSEN_SOUNDS_PER_STATION 6
#define SHINKANSEN_FOLDER_NUMBER 2
#define SHINKANSEN_BASE_SOUND 1

#define SHINKANSEN_AMBIENT_STATION_TOTAL 1 /*!< Currently, just Kobe */
#define SHINKANSEN_AMBIENT_SOUNDS_PER_STATION 1
#define SHINKANSEN_AMBIENT_FOLDER_NUMBER 4
#define SHINKANSEN_AMBIENT_BASE_SOUND 1



/* These log interfaces are compatible with the advanced thread-safe
 * logging API made available by FARGOS Development, LLC.
 */
#if LOG_ENABLED
#include <Streaming.h>
#if LOG_ENABLED == 2
#define LOG_COUT(level) Serial << F(__FILE__) << F(":") << __LINE__ << F("\t") << F(#level) << F("\t")
#else
#define LOG_COUT(level) Serial << F(":") << __LINE__ << F("\t") << F(#level) << F("\t")

#endif
#define LOG_ENDLINE endl

#endif
#if OLED_ENABLED
//#include <Adafruit_SSD1306.h>
//#include <splash.h>
#include <Adafruit_GFX.h>
#include <ArducamSSD1306.h>


#define SCREEN_WIDTH 128 /*!< OLED display width, in pixels */
#define SCREEN_HEIGHT 64 /*!< OLED display height, in pixels */

ArducamSSD1306 display(16); /*!< I2C interface, pin 16=reset */

#endif

#include <SoftwareSerial.h>

#define USE_ORIG_DFPLAYER 1
#if USE_ORIG_DFPLAYER == 1
#include <DFRobotDFPlayerMini.h>
#else
#include <DFMiniMp3.h>
#endif


#if DUMP_STATE_INTERVAL_SECONDS != 0
static uint32_t lastDumpInterval;
#endif

/*! \brief Millisecond clock supporting rollover using generation counts.
 *
 * \note The 32-bit millisecond count rolls over every 49 days.
 */
class ClockWithRollover
{
public:
    static uint32_t maxRolloverCount;
    static uint32_t lastMilliseconds;
    uint32_t        lastClockValue;
    uint32_t        lastRolloverCount;

public:
    /*! \brief Container for milliseconds
     */
    class MillisecondValue
    {
    public:
        uint32_t  value;

        explicit MillisecondValue(uint32_t t) :
            value(t)
        {}
    };

    /*! \brief Container for microseconds
     */
    class MicrosecondValue
    {
    public:
        uint32_t  value;

        explicit MicrosecondValue(uint32_t t) :
            value(t)
        {}
    };

    void resetClock()
    {
        lastClockValue = 0;
        lastRolloverCount = 0;
    }

    ClockWithRollover()
    {
        resetClock();
    }

    explicit ClockWithRollover(MillisecondValue ms)
    {
        lastClockValue = ms.value;
        lastRolloverCount = maxRolloverCount;
    }

    bool isClockSet() const
    {
        return ((lastClockValue != 0) || (lastRolloverCount != 0));
    }

    uint32_t getIntervalMilliseconds() const
    {
        return (lastClockValue);
    }

    bool operator<(const ClockWithRollover c) const
    {
        if (lastRolloverCount > c.lastRolloverCount) {
            return (false);
        }
        if (lastRolloverCount < c.lastRolloverCount) {
            return (true);
        }
        // same epoch
        if (lastClockValue >= c.lastClockValue) {
            return (false);
        }
        return (true);
    }

    bool operator<=(const ClockWithRollover c) const
    {
        if (lastRolloverCount > c.lastRolloverCount) {
            return (false);
        }
        if (lastRolloverCount < c.lastRolloverCount) {
            return (true);
        }
        // same epoch
        if (lastClockValue > c.lastClockValue) {
            return (false);
        }
        return (true);
    }

    bool operator<(const MillisecondValue t) const
    {
        if (lastClockValue < t.value) {
            return (true);
        }
        return (false);
    }

    bool operator<(const MicrosecondValue t) const
    {
        if (lastClockValue < (t.value / 1000)) {
            return (true);
        }
        return (false);
    }

    ClockWithRollover &operator=(const MillisecondValue v)
    {
        if (v.value < lastClockValue) {
            lastRolloverCount += 1;
            if (lastRolloverCount > maxRolloverCount) {
                maxRolloverCount = lastRolloverCount;
            }
        }
        lastClockValue = v.value;
        return (*this);
    }

    ClockWithRollover &operator=(const MicrosecondValue v)
    {
        if ((v.value / 1000) < lastClockValue) {
            lastRolloverCount += 1;
            if (lastRolloverCount > maxRolloverCount) {
                maxRolloverCount = lastRolloverCount;
            }
        }
        lastClockValue = v.value;
        return (*this);
    }

    int32_t operator-(const ClockWithRollover arg) const
    {
        int32_t result = lastClockValue - arg.lastClockValue;
        int32_t rolloverDiff = lastRolloverCount - arg.lastRolloverCount;
        if (lastRolloverCount == 0) {
            return (result);
        }
        if (rolloverDiff < 0) { // arg is more recent
            if (rolloverDiff < -1) {
                return (0x80000000UL);
            }
            return (result - (~static_cast<int32_t>(0) - 1));
        } else {
            if (rolloverDiff > 1) {
                return (0x7fffffffUL);
            }
            return (result + (~static_cast<int32_t>(0) + 1));
        }
    }

    uint32_t addMilliseconds(uint32_t ms)
    {
        uint32_t newVal = lastClockValue + ms;
        if (newVal < lastClockValue) { // rollover occurred
            lastRolloverCount += 1;
            if (lastRolloverCount > maxRolloverCount) {
                maxRolloverCount = lastRolloverCount;
            }
        }
        lastClockValue = newVal;
        return (newVal);
    }

    uint32_t addMilliseconds(const MillisecondValue v)
    {
        return (addMilliseconds(v.value));
    }

    uint32_t getCurrentMilliseconds()
    {
        uint32_t ms = millis();
        lastMilliseconds = ms;
        MillisecondValue t(ms);
        *this = t;
        return (ms);
    }

    uint32_t getCurrentMicroseconds()
    {
        uint32_t microseconds = micros();
        uint32_t ms = microseconds / 1000;
        lastMilliseconds = ms;
        MillisecondValue t(ms);
        *this = t;
        return (microseconds);
    }
}; // end class ClockWithRollover

uint32_t ClockWithRollover::maxRolloverCount;
uint32_t ClockWithRollover::lastMilliseconds;

#if ENABLE_LED_BLINK_NOTIFICATIONS
/*! \brief Repeatedly flash an LED in a user-defined sequence.
 * Useful for providing a visual indication of an error condition.
 *
 * \note Limited to 127 flashes and 255 elements due to use of
 * 8 bit values.
 */
class BlinkLEDinSequence
{
protected:
    enum {
        DURATION_OF_FLASH = 500,
        DELAY_BETWEEN_FLASHES = 250,
        DELAY_BETWEEN_ELEMENTS = 1000,
        DELAY_BETWEEN_SEQUENCES = 3000
    };
    uint32_t        nextEventTime;
    const uint8_t   *flashList;
    uint8_t         sequenceTotal;
    uint8_t         currentSequence;
    uint8_t         flashesRemaining;
    uint8_t         currentMode;
    const uint8_t   LEDpin;

public:

    /*! \brief Construct a state object capable of blinking an LED
     * in a series of patterns.
     *
     * \sa setFlashes()
     */
    explicit BlinkLEDinSequence(uint8_t pin = LED_BUILTIN, uint8_t total = 0, const uint8_t *flashes = nullptr) :
        LEDpin(pin),
        flashList(flashes),
        sequenceTotal(total)
    {
        nextEventTime = 0;
        currentSequence = total - 1;
        flashesRemaining = 1;
        currentMode = LOW;

        pinMode(LEDpin, OUTPUT);
        digitalWrite(LEDpin, LOW);
    }

    /*! \brief Determine if a blink sequence has been defined.
     */
    bool isDefined() const
    {
        return (sequenceTotal != 0);
    }

    /*! \brief Set a sequence of requested blink patterns.
     *
     * \param total specifies the number of elements in the blink
     * sequence.
     * \param flashes points to an array of blink counts.
     *
     * By default, blink sequences are repeeated for as long as
     * processBlinking() is called.
     * The value of 255 is recognized as marking the end of a sequence
     * that should only be processed once.
     * A twice-as-long on period can be specified by adding 128 to an
     * element's blink total.  A longer off period can be specified
     * with a value of 128.
     */
    void setFlashes(uint8_t total, const uint8_t *flashes)
    {
        nextEventTime = 0;
        flashList = flashes;
        sequenceTotal = total;
        currentSequence = total - 1;
        flashesRemaining = 1;
        currentMode = LOW;
    }

    void turnOff()
    {
        sequenceTotal = 0;
        currentMode = LOW;
        digitalWrite(LEDpin, LOW);
    }

    /*! \brief Handle turning on/off of LED blink sequence.
     *
     * \param now specifies the current time as obtained from millis().
     *
     * This routine is expecteed to be called repeatedly from loop().
     * There are no calls to delay().
     */
    uint8_t processBlinking(uint32_t now)
    {
        if (sequenceTotal == 0) { // nothing set
            return (0);
        }
        if (nextEventTime > now) { // time not yet reached
            return (0);
        }
        // event time reached
        uint8_t durationScaler = (flashesRemaining & 128) ? 2 : 1;
        if (currentMode == LOW) { // was off, turn on
            digitalWrite(LEDpin, HIGH);
            currentMode = HIGH;
            nextEventTime = now + (DURATION_OF_FLASH * durationScaler);
        } else { // was on, turn off
            digitalWrite(LEDpin, LOW);
            currentMode = LOW;
            flashesRemaining -= 1;
            if ((flashesRemaining & 127) == 0) { // move to next element
                currentSequence += 1;
                if (currentSequence >= sequenceTotal) {
                    // reached end of sequence, restart at beginning
                    nextEventTime = now + DELAY_BETWEEN_SEQUENCES;
                    currentSequence = 0; // restart sequence
                } else { // in the middle of a sequence
                    nextEventTime = now + DELAY_BETWEEN_ELEMENTS;
                }
                flashesRemaining = flashList[currentSequence];
                if (flashesRemaining == 255) { // one time sequence
                    turnOff();
                } else if ((flashesRemaining & 127) == 0) { // introduce pause
                    currentMode = HIGH; // lie, really still off
                    flashesRemaining = 1;
                    nextEventTime = now + DELAY_BETWEEN_FLASHES * durationScaler;
                }
            } else {
                nextEventTime = now + DELAY_BETWEEN_FLASHES;
            }
            return (1);
        } // end if end-of-event reached
    } // end processBlinking()

}; // end class BlinkLEDinSequence


static const uint8_t alarmFlashes_noProgress[] = { 2, 2, 1 };
static const uint8_t alarmFlashes_endOfCycle[] = { 2, 8, 1 };
static BlinkLEDinSequence LEDalarm;

#endif


/*! \brief Exponential moving average smoother for values
 * read from analog-to-digital converter.
 *
 * \param MAX_ANALOG_VALUE is a template parameter that specifies the
 * upper bound of the range of values.  The correct value depends upon
 * the analog-to-digital converter in use.  For Arduino hardware with
 * 10 bits of precision, the value range is [0, 1023].
 * \param MIN_ANALOG_JITTER specifies the amount of change
 * that must occur for a new value to be recorded.
 * \param INVERT_SCALE permits the normal [0, MAX_ANALOG_VALUE] to be
 * flipped and reported as [MAX_ANALOG_VALUE, 0].  This is useful
 * if the analog device outputs signal levels in the reverse of what
 * is desired.
 */
template <uint16_t MAX_ANALOG_VALUE = 1023, uint16_t MIN_ANALOG_JITTER = 10, bool INVERT_SCALE = false> class AnalogReadingSmoother
{
public:
    enum {
        ANALOG_VALUE_RANGE = MAX_ANALOG_VALUE + 1 //!< expose template parameter value
    };

protected:
    enum {
        UPPER_BOUND_SCALER = (INVERT_SCALE ? 1 : 0),
        SIGN_OF_READING = (INVERT_SCALE ? -1 : 1)
    };
    float       EMA_a; //!< a coefficient for exponential moving average
    uint16_t    EMA_S; //!< S coefficient for exponential moving average
    uint16_t    digitizedValue;
    uint16_t    lastRawReading;

    const uint8_t     analogPin;

public:
    /*! \brief Construct an analog reading smoother.
     *
     * \param pin specifies the analog pin from which results will
     * be obtained using analogRead().
     */
    explicit AnalogReadingSmoother(uint8_t pin) :
        analogPin(pin)
    {
        EMA_a = 0.6;
        EMA_S = 0;
        digitizedValue = 0;
        lastRawReading = 0;
    }

    /*! \brief Read analog sensor, invert if needed, and smooth
     * to reduce jitter.
     *
     * \return the smoothed digitized value is returned.
     */
    uint16_t updateValue()
    {
        int16_t curVal = analogRead(analogPin);
        /* normally, UPPER_BOUND_SCALER=0, SIGN_OF_READING=1,
         * which keeps curVal unchanged.  To invert the scale,
         * UPPER_BOUND_SCALER=1 and SIGN_OF_READING=-1.
         * As these are template parameters, the formula simplifies
         * at compile time to either a simple assignment or a subtraction
         * from a constant.
         */
        curVal = (UPPER_BOUND_SCALER * MAX_ANALOG_VALUE) + (SIGN_OF_READING * curVal);
        int16_t changeAmt = (curVal <= digitizedValue) ?
                            digitizedValue - curVal :
                            curVal - digitizedValue;

        if (changeAmt > MIN_ANALOG_JITTER) {
            EMA_S = (EMA_a * curVal) + ((1 - EMA_a) * EMA_S);
            digitizedValue = EMA_S;
#if LOG_ENABLED && DEBUG_ANALOG_CONVERSION
            LOG_COUT(change) << F("Analog changed ") << lastRawReading <<
                             F(" to ") << curVal <<
                             F(" smoothed=") << digitizedValue << LOG_ENDLINE;
#endif
            lastRawReading = curVal;
        }
        return (digitizedValue);
    }

    /*! \brief Retrieve last digitized value.
     *
     * \sa updateValue()
     */
    uint16_t getValue() const
    {
        return (digitizedValue);
    }
}; // end class AnalogReadingSmoother<>


/*! \brief Poll motion-trigger sensor
 *
 * Button press can be either short or long.
 */
class MotionTriggerState
{
public:
    enum {
        LONG_BUTTON_PRESS_TIME_MS = 2 * 1000 //!< minimum milliseconds button must be held down
    };

    uint32_t lastActivateModeTime; //!< milliseconds last change was processed
    uint32_t lastButtonPressTime; //!< milliseconds since button appeared pressed
    enum eButtonState { NOT_PRESSED = 0, INITIAL_PRESS, SHORT_PRESS,
                        LONG_PRESS, LONG_PRESS_DETECTED
                      } lastActivateModeSet; //!< set in loop()
    uint8_t lastActivateModeRead; /*!< set ininterrupt1Routine() */
    const uint8_t triggerPin;

    MotionTriggerState(uint8_t pin) :
        triggerPin(pin)
    {
        lastActivateModeSet = NOT_PRESSED;
        lastActivateModeTime = 0;
        lastActivateModeRead = NOT_PRESSED;
        lastButtonPressTime = 0;
    }

    /*! \brief Read button state and potentially determine length
     * of a button press.
     *
     */
    enum eButtonState pollButton(uint32_t currentTime)
    {
        lastActivateModeRead = digitalRead(triggerPin);
        if (lastActivateModeRead == LOW) { // button was pressed
            if (lastActivateModeSet == NOT_PRESSED) { // no event in process
                lastButtonPressTime = currentTime;
                lastActivateModeSet = INITIAL_PRESS;
#if LOG_ENABLED && DEBUG_MOTION_SENSOR
                LOG_COUT(change) << F("INITIAL_PRESS") << LOG_ENDLINE;
#endif
            } else {   // button press in progress
                uint32_t pressDuration = currentTime - lastButtonPressTime;
                if (pressDuration >= LONG_BUTTON_PRESS_TIME_MS) {
                    if (lastActivateModeSet == INITIAL_PRESS) {
                        lastActivateModeSet = LONG_PRESS_DETECTED;
#if LOG_ENABLED && DEBUG_MOTION_SENSOR
                        LOG_COUT(change) << F("LONG_PRESS") << LOG_ENDLINE;
#endif
                        return (LONG_PRESS);
                    }
                }
            }
        } else { // button/motion idle
            if (lastActivateModeSet != NOT_PRESSED) {
                // mechanical switches tend to have a series of make/break
                // signals as the contacts come together/separate,
                // so a minimal amount of time is required to treat
                // a signal as real
                enum { MIN_DEBOUNCE_TIME_MS = 200 };  //!< minimum idle time
                uint32_t pressDuration = currentTime - lastButtonPressTime;
                if (pressDuration > MIN_DEBOUNCE_TIME_MS) {
#if LOG_ENABLED && DEBUG_MOTION_SENSOR
                    LOG_COUT(change) << F("NOT_PRESSED ") << pressDuration << LOG_ENDLINE;
#endif
                    lastActivateModeSet = NOT_PRESSED;
                    if (lastActivateModeSet == INITIAL_PRESS) {
                        return (SHORT_PRESS);
                    }
                }
            }
        }
        return (lastActivateModeSet);
    }
}; // end class MotionTriggerState

static AnalogReadingSmoother<1023> analogDial(CYCLE_DURATION_ANALOG_PIN);
static MotionTriggerState motionTrigger(PIN_INTERRUPT1);

static ClockWithRollover   startOfActiveCycle;
static ClockWithRollover   endOfActiveCycle;

/*! \brief Update endOfActiveCycle from current value of cycle duration
 * obtained from analog-to-digital conversion.
 */
static void updateCycleEndTime()
{
    endOfActiveCycle = startOfActiveCycle;
#if ANALOG_DIAL_CONTROLS_SPEED == 0
    // analog dial is used to determine length of operating cycle
    // possible range should be [0-1023]
    uint32_t minutesPermitted = (analogDial.getValue() * MAX_OPERATION_TIME_IN_MINUTES) / analogDial.ANALOG_VALUE_RANGE;
    minutesPermitted += 1;
#if LOG_ENABLED
    LOG_COUT(info) << F("minutes=") << minutesPermitted << LOG_ENDLINE;
#endif
#else
    uint32_t minutesPermitted = MAX_OPERATION_TIME_IN_MINUTES;
#endif
    endOfActiveCycle.addMilliseconds(minutesPermitted * (static_cast<uint32_t>(1000) * 60));
}

/*! \brief Per-track desired sound state
 *
 * \note All sounds for a particular track must come from the same
 * sound folder.  Multiple TrackSoundState objects can be used
 * to support several folders.
 */
class TrackSoundState
{
public:
    enum eSoundCycleDirection {
        NO_CHANGE,
        NEW_CYCLE_ONLY,
        PRIOR_STATION_THEN_NEW_CYCLE,
        NEXT_STATION_THEN_NEW_CYCLE,
        NEW_CYCLE_THEN_PRIOR_STATION,
        NEW_CYCLE_THEN_NEXT_STATION,
        USE_DEFAULT
    };
    /*! \brief Container to identify file on DF Mini Player
     * filesystem by file number and folder number.
     */
    struct SoundID {
        uint16_t    fileNumber;
        uint8_t     folderNumber;
    };
protected:
    const uint16_t  baseSoundEffectNumber;
    const uint8_t   totalStationsMinus1;
    const uint8_t   soundsPerStation;
    const uint8_t   soundFolderNumber;
#if STORE_TRACK_NUMBER_IN_SOUND_STATE == 1
    const uint8_t   forTrackNumber;
#endif
    eSoundCycleDirection soundCycleMode;
    uint8_t         storageForCurrentStation;
    uint8_t         storageForCurrentSound;

    uint8_t         *currentStation;
    uint8_t         *currentSoundInStation;

    bool startNextSoundCycle()
    {
        if (*currentSoundInStation < (soundsPerStation - 1)) {
            *currentSoundInStation += 1;
            return (false);
        }
        // wrap around to restart
        *currentSoundInStation = 0;
        return (true);
    }

public:
    TrackSoundState(uint8_t trackNumber, uint8_t maxStations,
                    uint8_t maxPerStation,
                    uint8_t soundFolder, uint16_t baseFileNumber = 100) :
        baseSoundEffectNumber(baseFileNumber),
        totalStationsMinus1(maxStations - 1),
        soundsPerStation(maxPerStation),
        soundFolderNumber(soundFolder),
#if STORE_TRACK_NUMBER_IN_SOUND_STATE == 1
        forTrackNumber(trackNumber),
#endif
        currentStation(&storageForCurrentStation),
        currentSoundInStation(&storageForCurrentSound)
    {
        soundCycleMode = NEXT_STATION_THEN_NEW_CYCLE;
        *currentStation = 0;
        *currentSoundInStation = 0;
    }

    void setCycleMode(eSoundCycleDirection mode)
    {
        soundCycleMode = mode;
    }

    void setCurrentStationStorage(uint8_t *addr)
    {
        currentStation = (addr == nullptr) ?
                         &storageForCurrentStation : addr;
    }

    void setCurrentSoundStorage(uint8_t *addr)
    {
        currentSoundInStation = (addr == nullptr) ?
                                &storageForCurrentSound : addr;
    }

    /*! \brief Enable synchronization of station number with another
     * TrackSoundState object.
     */
    void synchronizeStationsWith(TrackSoundState &obj, eSoundCycleDirection mode)
    {
        currentStation = obj.currentStation;
        soundCycleMode = mode;
    }

    /*! \brief Enable synchronization of relative cycle with another
     * TrackSoundState object.
     */
    void synchronizeAlternatesWith(TrackSoundState &obj, eSoundCycleDirection mode)
    {
        currentSoundInStation = obj.currentSoundInStation;
        soundCycleMode = mode;
    }

    SoundID getNextStationSound(eSoundCycleDirection dir = USE_DEFAULT)
    {
        if (dir == USE_DEFAULT) {
            dir = soundCycleMode;
        }
        uint16_t fileNum = baseSoundEffectNumber +
                           (*currentStation * soundsPerStation) + *currentSoundInStation;
#if LOG_ENABLED && (DEBUG_SOUND_CONTROLLER > 1)
        LOG_COUT(info) << F("getNextStation dir=") << dir <<
                       F(" station=") << *currentStation <<
                       F(" sounds=") << soundsPerStation <<
                       F(" curInStation=") << *currentSoundInStation <<
                       F(" fileNum=") << fileNum << LOG_ENDLINE;
#endif
        bool inhibitNewCycle = false;
        switch (dir) {
        case NEW_CYCLE_THEN_NEXT_STATION:
            inhibitNewCycle = true;
            startNextSoundCycle();
        // fall through
        case NEXT_STATION_THEN_NEW_CYCLE:
            // moving forward
            if (*currentStation < totalStationsMinus1) {
                *currentStation += 1;
                inhibitNewCycle = true;
            } else {
                *currentStation = 0;
            }
            break;
        case NEW_CYCLE_THEN_PRIOR_STATION:
            inhibitNewCycle = true;
            startNextSoundCycle();
        // fall through
        case PRIOR_STATION_THEN_NEW_CYCLE:
            // moving backwards
            if (*currentStation > 0) {
                *currentStation -= 1;
                inhibitNewCycle = true;
            } else {
                *currentStation = totalStationsMinus1;
            }
            break;
        case NEW_CYCLE_ONLY:
            // nothing to do
            break;
        case NO_CHANGE:
        default:
            // no change to station nor sound within cycle
            inhibitNewCycle = true;
            break;
        } // end switch dir
        if (inhibitNewCycle == false) {
            startNextSoundCycle();
        }
        SoundID result;
        result.folderNumber = soundFolderNumber;
        result.fileNumber = fileNum;
        return (result);
    }

#if STORE_TRACK_NUMBER_IN_SOUND_STATE == 1
    uint8_t getTrackNumber() const
    {
        return (forTrackNumber);
    }
#endif
}; // end class TrackSoundState

/*! \brief Sensor readings for a specific track

*/
template <uint8_t SENSORS_PER_TRACK> class TrackSensorStates
{
public:
    enum eChangeFlags {
        TRAIN_ARRIVED_FLAG = 1,
        TRAIN_DEPARTED_FLAG = 2
    };
    enum eMasks {
        SUBSCRIPT_MASK = ((1 << SENSORS_PER_TRACK) - 1)
    };

    /* lowest SENSORS_PER_TRACK bits correspond to a change having been
     * made on the corresponding sensor.  Each progressively more
     * significant pair of bits indicates arrived/departed/clear states.
     */
    static constexpr uint16_t sensorChangeMask(uint8_t sensor,
            eChangeFlags movement, uint16_t extraBits = 0)
    {
        return ((movement << (SENSORS_PER_TRACK + 1 + (sensor * 2))) | extraBits);
    }

    static uint8_t extractFirstSensorSubscript(uint16_t mask)
    {
        uint16_t testMask = (TRAIN_ARRIVED_FLAG | TRAIN_DEPARTED_FLAG) << (SENSORS_PER_TRACK + 1);
        for (uint8_t i = 0; i < SENSORS_PER_TRACK; i += 1) {
            if ((testMask & mask) != 0) {
                return (i);
            }
            testMask <<= 2;
        }
        return (SENSORS_PER_TRACK);
    }

    static constexpr uint16_t extractSensorChangeFlags(uint16_t mask, uint8_t sensor)
    {
        return ((mask >> (SENSORS_PER_TRACK + 1 + (sensor * 2))) & (TRAIN_ARRIVED_FLAG | TRAIN_DEPARTED_FLAG));
    }

private:
    enum {
        ACTIVE = LOW,   //!< Hall effect sensor is low in presence of magnetic field
        NOT_ACTIVE = HIGH, //!< Hall effect sensor is high when no magnetic field is present
        NEVER_READ = 64
    };
    uint8_t   trackSensorPin[SENSORS_PER_TRACK];
    const uint8_t   forTrackNumber;
    uint8_t   trackSensorState[SENSORS_PER_TRACK];
    uint8_t  lastReportedState[SENSORS_PER_TRACK];

public:
    TrackSensorStates(uint8_t forTrack, const uint8_t sensorPins[SENSORS_PER_TRACK]) :
        forTrackNumber(forTrack)
    {
        for (uint8_t i = 0; i < SENSORS_PER_TRACK; i += 1) {
            trackSensorPin[i] = sensorPins[i];
            trackSensorState[i] = NEVER_READ;
            lastReportedState[i] = 0;
        }
    }

    bool readSensor(uint8_t pinSubscript)
    {
        uint8_t state = digitalRead(trackSensorPin[pinSubscript]);
        if (state == trackSensorState[pinSubscript]) { // no change
            return (false);
        }
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
        LOG_COUT(info) << F("CHANGE ") << F(" track=") << forTrackNumber <<
                       F(" sub=") << pinSubscript <<
                       F(" pin=") << trackSensorPin[pinSubscript] <<
                       F(" prior=") << trackSensorState[pinSubscript] <<
                       F(" state=") << state << LOG_ENDLINE;
#endif
        trackSensorState[pinSubscript] = state; // update
        return (true);
    }

    // If ALL sensors are simultaneously grounded, treat track as disabled.
    // Normally, at most one should be LOW and the rest HIGH.
    bool isTrackActive() const
    {
        bool someSensorNotLOW = false;
        for (uint8_t i = 0; i < SENSORS_PER_TRACK; i += 1) {
            uint8_t state = digitalRead(trackSensorPin[i]);
            if (state != LOW) {
                someSensorNotLOW = true;
#if (LOG_ENABLED) && (DEBUG_TRACK_SENSORS > 2)
            } else { // sensor reads low, active
                LOG_COUT(info) << F(" track=") << forTrackNumber <<
                               F(" sub=") << i <<
                               F(" pin=") << trackSensorPin[i] <<
                               F(" state=") << state <<
                               F(" LOW") << LOG_ENDLINE;
#endif
            }
        }
        return (someSensorNotLOW);
    }

    /*! \brief Read state of all track presence sensors.
     *
     * \return A bitmask indicating which sensors have changed state
     * is returned.
     */
    // NOTE: return type would need to be promoted if
    // more than 5 sensors are used
    uint16_t readAllSensors()
    {
        uint16_t result = 0;
        for (uint8_t i = 0; i < SENSORS_PER_TRACK; i += 1) {
            bool changed = readSensor(i);
            if (changed == false) { // sensor updated
                continue;
            }
            if (trackSensorState[i] == ACTIVE) {
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
                LOG_COUT(info) << F(" track=") << forTrackNumber <<
                               F(" sub=") << i <<
                               F(" pin=") << trackSensorPin[i] << F(" ACTIVE") << LOG_ENDLINE;
#endif
                uint16_t mask = sensorChangeMask(i, TRAIN_ARRIVED_FLAG);
                if ((lastReportedState[i] & mask) == 0) {
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
                    LOG_COUT(info) << F(" track=") << forTrackNumber <<
                                   F(" TRAIN ARRIVED") << F(" sensor=") << i <<
                                   F(" newMask=") << mask <<
                                   F(" previousState=") << lastReportedState[i] << LOG_ENDLINE;
#endif
                    lastReportedState[i] = mask;
                    result |= (1 << i) | mask;
                }
            } else { // train not at sensor location
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
                LOG_COUT(info) << F(" track=") << forTrackNumber <<
                               F(" sub=") << i <<
                               F(" pin=") << trackSensorPin[i] <<
                               F(" NOT_ACTIVE") << LOG_ENDLINE;
#endif
                if ((lastReportedState[i] & sensorChangeMask(i, TRAIN_ARRIVED_FLAG)) != 0) { // had arrived
                    uint16_t mask = sensorChangeMask(i, TRAIN_DEPARTED_FLAG); // assert it has now departed
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
                    LOG_COUT(info) << F(" track=") << forTrackNumber << F(" TRAIN_DEPARTED") << F(" sub=") << i << F(" mask=") << mask << F(" prev=") << lastReportedState[i] << LOG_ENDLINE;
#endif
                    lastReportedState[i] = mask; // clear TRAIN_ARRIVED_FLAG
                    result |= (1 << i) | mask;
                }
            } /// end else train not at sensor location
        } // end for each sensor
        return (result);
    } // end readAllSensors()

}; // end class TrackSensorStates

/*! \brief Interface for client event handlers
*/
class EventClient
{
public:
    /*! \brief Interface for asynchronous events

        \param ok 0=failed, 1=queued, 2=completed
    */
    virtual void handleEvent(const ClockWithRollover eventTime, uint8_t ok, uint32_t data) = 0;

}; // end class EventClient

#define F2C(s) s

#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
static const char *audioStatusDetail(char bfr[], int bfrSize, uint8_t type, int value)
{
    switch (type) {
    case TimeOut:
        return (F2C("Time Out!"));
    case WrongStack:
        return (F2C("Stack Wrong!"));
    case DFPlayerCardInserted:
        return (F2C("Card Inserted!"));
    case DFPlayerCardRemoved:
        return (F2C("Card Removed!"));
    case DFPlayerCardOnline:
        return (F2C("Card Online!"));
    case DFPlayerUSBInserted:
        return (F2C("USB Inserted!"));
    case DFPlayerUSBRemoved:
        return (F2C("USB Removed!"));
        break;
    case DFPlayerPlayFinished:
        snprintf(bfr, bfrSize, F2C("Number: %d Play Finished!"), value);
        return (bfr);
    case DFPlayerError:
        switch (value) {
        case Busy:
            return (F2C("Card not found"));
        case Sleeping:
            return (F2C("Sleeping"));
        case SerialWrongStack:
            return (F2C("Get Wrong Stack"));
        case CheckSumNotMatch:
            return (F2C("Check Sum Not Match"));
        case FileIndexOut:
            return (F2C("File Index Out of Bound"));
        case FileMismatch:
            return (F2C("Cannot Find File"));
        case Advertise:
            return (F2C("In Advertise"));
        default:
            break;
        }
        break;
    default:
        break;
    }
    return (F2C("UNKNOWN"));
}
#endif

/*! \brief Global controller for playing sounds.
 *
 * Maintains queue of pending sound requests, notifies clients when
 * a sound completes playing.
 *
 * \note Must call init() after constructing the object to finish setup.
 */
class TrainSoundController
{
public:
    enum eSoundStates {
        SOUND_BUSY = SOUND_ACTIVE,
        SOUND_NOT_BUSY = SOUND_IDLE,
        SOUND_PENDING_BUSY = 16,
        SOUND_ERROR = 64,
        SOUND_NEVER_READ = 255
    };
    SoftwareSerial      audioSerial;
#if USE_ORIG_DFPLAYER == 1
    DFRobotDFPlayerMini audioPlayer;
#else
    class MP3notify
    {
    } callbackState;
    DFMiniMp3<SoftwareSerial, MP3notify>    audioPlayer;
#endif
private:
    enum {
        MAX_SOUND_EVENTS = 4 // should be a power-of-2 to avoid division
    };
    struct SoundRequest {
        uint32_t          clientData;
        EventClient       *client;
        uint16_t          soundNumber;
        uint8_t           folderNumber;
        bool              playAttempted;
    } eventQueue[MAX_SOUND_EVENTS];
    uint32_t  soundStateChanges;
    const uint8_t   soundBusyPin;
    uint8_t   eventQueueHead;
    uint8_t   eventQueueTail;
    bool      queueChanged;

    uint8_t   lastSoundStateSet;
    uint8_t   lastSoundStateRead;

public:
    uint8_t waitForSoundState(uint8_t desiredState, uint32_t maxDelay)
    {
        uint32_t startDelay = millis();
        uint8_t lastState;
        do {
            lastState = digitalRead(soundBusyPin);
        } while ((lastState != desiredState) &&
                 ((millis() - startDelay) < maxDelay));
        return (lastState);
    }

private:
    bool playSoundInFolder(uint8_t folderNum, uint16_t soundNum)
    {
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("PLAY folder/track ") << folderNum << F("/") << soundNum << LOG_ENDLINE;
#endif
#if ENABLE_CONSOLE_COMMANDS > 1
        audioSerial.listen();
#endif
        lastSoundStateSet = SOUND_PENDING_BUSY;
        audioPlayer.playLargeFolder(folderNum, soundNum);
        //    audioPlayer.start();
#if ENABLE_CONSOLE_COMMANDS > 2
        LOG_COUT(error) << F("STOP LISTENING") << LOG_ENDLINE;
        audioSerial.stopListening();
#endif
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        while (audioPlayer.available()) {
            char bfr[128];
            LOG_COUT(info) << F("audioPlayer status=") <<
                           audioStatusDetail(bfr, sizeof(bfr), audioPlayer.readType(), audioPlayer.read()) << LOG_ENDLINE; // Print the detail message from DFPlayer to handle different errors and states.
        }
#endif
        // wait just long enough for BUSY pin to reflect status of request,
        // but give up if no change is detected within MAX_AUDIO_TIMEOUT
        uint8_t lastState = waitForSoundState(SOUND_BUSY, MAX_AUDIO_TIMEOUT);

        if (lastState != SOUND_BUSY) { // sound never busy
            return (false);
        }
        return (true);
    }

    bool hasQueueChanged() const
    {
        return (queueChanged);
    }

    bool processSoundHeadEvent(const ClockWithRollover now)
    {
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("TrainSoundController::processSoundHeadEvent") << LOG_ENDLINE;
#endif
        queueChanged = false;
        // if a sound is queued, play it
        if (eventQueueHead == eventQueueTail) {
            return (false); // empty queue
        }
        eventQueue[eventQueueHead].playAttempted = true;
        bool ok = playSoundInFolder(eventQueue[eventQueueHead].folderNumber, eventQueue[eventQueueHead].soundNumber);
        if (ok == false) { // could not play
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
            LOG_COUT(info) << F("failed PLAY request") << LOG_ENDLINE;
#endif
            // a request had been queued
            EventClient *client = eventQueue[eventQueueHead].client;
            if (client != nullptr) { // pass error back to client
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                LOG_COUT(info) << F("Call sound client handleEvent ok=") << ok <<
                               F(" data=") << eventQueue[eventQueueHead].clientData << LOG_ENDLINE;
#endif
                client->handleEvent(now, ok, eventQueue[eventQueueHead].clientData);
            }
            uint8_t nextHead = (eventQueueHead + 1) % MAX_SOUND_EVENTS;
            eventQueueHead = nextHead;
            queueChanged = true;
        }
        return (ok);
    }


public:
    bool init()
    {
        // setup software Serial port, hardware defaults to 9600 baud
        enum { AUDIO_BAUD_RATE = 9600 };
        audioSerial.begin(AUDIO_BAUD_RATE); // performs listen() as side-effect

        /* reset will cause annoying pop on startup, but it is needed
           for reliable operation with generic players.
        */
        bool rc = audioPlayer.begin(audioSerial, true, true); // don't bother to reset
        audioPlayer.reset();
#if LOG_ENABLED
        if (rc == false) {
            LOG_COUT(info) << F("Audio begin failed") << LOG_ENDLINE;
            return (false);
        }
        LOG_COUT(info) << F("setTimeout") << LOG_ENDLINE;
#endif
        audioPlayer.setTimeOut(MAX_AUDIO_TIMEOUT);
//      LOG_COUT(info) << F("outputDevice") << LOG_ENDLINE;
        audioPlayer.outputDevice(DFPLAYER_DEVICE_SD);
//      LOG_COUT(info) << F("waitAvail") << LOG_ENDLINE;
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);
//      LOG_COUT(info) << F("enableDAC") << LOG_ENDLINE;
        audioPlayer.enableDAC();
//      LOG_COUT(info) << F("waitAvail") << LOG_ENDLINE;
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);
#if LOG_ENABLED
        LOG_COUT(info) << F("Enabled DAC") << LOG_ENDLINE;
        LOG_COUT(info) << F("set vol") << LOG_ENDLINE;
#endif
        audioPlayer.volume(30); // Max volume
#if LOG_ENABLED
        LOG_COUT(info) << F("waitAvail") << LOG_ENDLINE;
#endif
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);

        delay(1000);
#if LOG_ENABLED
        LOG_COUT(info) << F("EQ=") << audioPlayer.readEQ() << LOG_ENDLINE; //read EQ setting
        LOG_COUT(info) << F("waitAvail") << LOG_ENDLINE;
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);
        LOG_COUT(info) << F("File counts=") << audioPlayer.readFileCounts(DFPLAYER_DEVICE_SD) << LOG_ENDLINE; //read all file counts in SD card
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);

        LOG_COUT(info) << F("Current file number=") << audioPlayer.readCurrentFileNumber() << LOG_ENDLINE; //read current play file number
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);

        LOG_COUT(info) << F("File counts in folder 1=") << audioPlayer.readFileCountsInFolder(1) << LOG_ENDLINE; //read file counts in folder SD:/01
        audioPlayer.waitAvailable(MAX_AUDIO_ACK_TIMEOUT);
        LOG_COUT(info) << F("status=") << audioPlayer.readState() << LOG_ENDLINE;
#endif

#if ENABLE_CONSOLE_COMMANDS > 2
        LOG_COUT(error) << F("STOP LISTENING") << LOG_ENDLINE;
        audioSerial.stopListening();
#endif
        return (rc);
    }

    TrainSoundController(uint8_t receivePin, uint8_t transmitPin, uint8_t busyPin) :
        audioSerial(receivePin, transmitPin),
#if USE_ORIG_DFPLAYER == 1
#else
        audioPlayer(audioSerial, callbackState),
#endif
        soundBusyPin(busyPin)
    {
        soundStateChanges = 0;
        eventQueueHead = 0;
        eventQueueTail = 0;
        lastSoundStateRead = SOUND_NEVER_READ; // never read
        lastSoundStateSet = SOUND_NEVER_READ; // never read
        queueChanged = false;
        // must call init() to complete setup
    }

    ~TrainSoundController() {}

    bool waitUntilAvailable(uint32_t milliseconds)
    {
#if ENABLE_CONSOLE_COMMANDS > 1
        audioSerial.listen();
#endif
        bool ready = audioPlayer.waitAvailable(milliseconds);
#if ENABLE_CONSOLE_COMMANDS > 2
        LOG_COUT(error) << F("STOP LISTENING") << LOG_ENDLINE;
        audioSerial.stopListening();
#endif
        return (ready);
    }

    /*! \brief Queue up a sound to be played.

    */
    bool queueSoundRequest(const ClockWithRollover now,
                           uint8_t folderNumber, uint16_t soundNumber,
                           EventClient *client = nullptr,
                           uint32_t clientData = 0,
                           bool okToProcessNow = true)
    {
        uint8_t nextTail = (eventQueueTail + 1) % MAX_SOUND_EVENTS;
        if (nextTail == eventQueueHead) { // queue overflow
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
            LOG_COUT(warn) << F("Sound Queue overflow") << LOG_ENDLINE;
#endif
            return (false);
        }
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("sound head=") << eventQueueHead << F(" tail=") << eventQueueTail << F(" okToProcess=") << okToProcessNow << F(" lastSoundState=") << lastSoundStateSet << LOG_ENDLINE;
#endif
        queueChanged = true;
        bool queueWasEmpty = (eventQueueHead == eventQueueTail);
        eventQueue[eventQueueTail].client = client;
        eventQueue[eventQueueTail].clientData = clientData;
        eventQueue[eventQueueTail].folderNumber = folderNumber;
        eventQueue[eventQueueTail].soundNumber = soundNumber;
        eventQueue[eventQueueTail].playAttempted = false;
        eventQueueTail = nextTail;
        if ((queueWasEmpty) && (okToProcessNow)) {
            if ((lastSoundStateSet == SOUND_NOT_BUSY) || (lastSoundStateSet == SOUND_NEVER_READ)) { // process sound
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                LOG_COUT(info)  << F("sound q empty, lastSoundState=") << lastSoundStateSet << LOG_ENDLINE;
#endif
                bool ok = processSoundHeadEvent(now);
                return (ok);
            }
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
            LOG_COUT(warn) << F("Queue was empty, but sound controller busy") << LOG_ENDLINE;
#endif
        }
        return (false);
    }

    uint8_t getLastSoundStateSet() const
    {
        return (lastSoundStateSet);
    }

    uint8_t getLastSoundStateRead() const
    {
        return (lastSoundStateSet);
    }

    void setLastSoundStateSet(uint8_t newState)
    {
        lastSoundStateSet = newState;
    }

    void setLastSoundStateRead(uint8_t newState)
    {
        lastSoundStateRead = newState;
    }

    /*! \brief Process BUSY signal transition from sound device
     *
     * \param now current time
     * \param newMode indicates BUSY state.
     */
    bool changeSoundState(const ClockWithRollover now, uint8_t newMode)
    {
        if ((lastSoundStateSet == newMode) && (queueChanged == false)) { // not really a change
            return (false);
        }
#if LOG_ENABLED && (DEBUG_SOUND_CONTROLLER > 1)
        LOG_COUT(info) << F("changeSoundState ") << newMode << LOG_ENDLINE;
#endif
        lastSoundStateSet = newMode;
        soundStateChanges += 1;
        if (newMode == SOUND_NOT_BUSY) { // now we are not busy
            queueChanged = false;
            if (eventQueueHead != eventQueueTail) { // a request had been queued
                queueChanged = true;
                if (eventQueue[eventQueueHead].playAttempted == false) {
                    /* playSoundInFolder() never called for current head,
                     * so sound request must have been queued while the playing of
                     * a sound file was already in progress.
                     */
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                    LOG_COUT(info) << F("sound never submitted") << LOG_ENDLINE;
#endif
                    // attempt to play this request since previous has now finished
                    bool ok = processSoundHeadEvent(now);
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                    LOG_COUT(info) << F("back from processSoundHeadEvent ok=") << ok << LOG_ENDLINE;
#endif
                    return (ok);
                }
                EventClient *client = eventQueue[eventQueueHead].client;
                if (client != nullptr) {
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                    LOG_COUT(info) << F("Call sound client handleEvent indicating sound played data=") << eventQueue[eventQueueHead].clientData << LOG_ENDLINE;
#endif
                    client->handleEvent(now, 2, eventQueue[eventQueueHead].clientData);
                }
                uint8_t nextHead = (eventQueueHead + 1) % MAX_SOUND_EVENTS;
                eventQueueHead = nextHead;
                if (eventQueueHead != eventQueueTail) {
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                    LOG_COUT(info) << F("call TrainSoundController::processSoundHeadEvent") << LOG_ENDLINE;
#endif
                    bool ok = processSoundHeadEvent(now);
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
                    LOG_COUT(info) << F("back from processSoundHeadEvent ok=") << ok << LOG_ENDLINE;
#endif
                    return (ok);
                }
            }
        }
        return (false);
    }

    bool processSoundQueue(const ClockWithRollover now)
    {
        if (eventQueueHead == eventQueueTail) { // no request is queued
            return (false);
        }
        bool result = changeSoundState(now, lastSoundStateSet);
        return (result);
    }

}; // end class TrainSoundController

TrainSoundController soundController(PIN_AUDIO_CONTROLLER_RX, PIN_AUDIO_CONTROLLER_TX, PIN_INTERRUPT0);


/*! \brief Simpler L298N interface.
 *
 * Drives the 3 pins associated with an L298N motor driver: two directional and
 * one pulse width modulation.
 */
class SimpleL298N
{
public:
    enum Direction {
        FORWARD = 0,
        BACKWARD = 1,
        UNCHANGED = 8,  //<! Not compatible with original L298N class
        NEVER_SET = 32  //<! Not compatible with original L298N class
    };
protected:
    Direction       desiredDirection;
    Direction       lastDirection;
    const uint8_t   pwmPin;
    const uint8_t   in1Pin;
    const uint8_t   in2Pin;
    uint8_t         desiredSpeed;
    uint8_t         reversedPolarity;

public:

    /*! \brief Construct simple interface to an L298N-based motor controller.
     */
    SimpleL298N(uint8_t speedPin, uint8_t dir1Pin, uint8_t dir2Pin, bool polarityFlipped = false) :
        pwmPin(speedPin),
        in1Pin(dir1Pin),
        in2Pin(dir2Pin)
    {
        desiredDirection = NEVER_SET;
        lastDirection = NEVER_SET;
        desiredSpeed = 0;
        reversedPolarity = polarityFlipped;

        pinMode(pwmPin, OUTPUT);
        pinMode(in1Pin, OUTPUT);
        pinMode(in2Pin, OUTPUT);
        analogWrite(pwmPin, 0);

    }

    /*! \brief Get administratively set speed.
     */
    uint8_t getSpeed() const
    {
        return (desiredSpeed);
    }

    /*! \brief Administratively set speed.
     *
     * \note This does NOT affect an active motor and only
     * takes effect if forward() or backward() are invoked,
     * either directly or via run().
     *
     * \sa changeSpeed()
     */
    void setSpeed(uint8_t newSpeed)
    {
        desiredSpeed = newSpeed;
    }

    /*! \brief Administratively set speed to 0.

       \note This does NOT affect an active motor and only
       takes effect if forward() or backward() are invoked,
       either directly or via run().

       \sa stopNowAndPowerDown()
    */
    void stop()
    {
        desiredSpeed = 0;
    }

    /*! \brief Get reversed polarity flag.
     */
    bool getReversedPolarity() const
    {
        return (reversedPolarity);
    }

    /*! \brief Set reversed polarity flag.
     */
    void setReversedPolarity(bool isReversed = true)
    {
        reversedPolarity = isReversed;
    }

    /*! \brief Get last administratively set direction
    */
    Direction getDirection() const
    {
        return (desiredDirection);
    }

    /*! \brief Turn on motor in forward direction.
    */
    void forward()
    {
        if (lastDirection != FORWARD) {
            analogWrite(pwmPin, 0); // force to disable
//LOG_COUT(info) << F("FORWARD FORCE PWM TO 0 p1=") << in1Pin << F(" p2=") << in2Pin << F(" pwm=") << pwmPin << LOG_ENDLINE;
            delay(1);
            lastDirection = FORWARD;
            desiredDirection = FORWARD;
            digitalWrite(in1Pin, HIGH);
            digitalWrite(in2Pin, LOW);
        }
        analogWrite(pwmPin, desiredSpeed);
    }

    /*! \brief Turn on motor in backward direction.
    */
    void backward()
    {
        if (lastDirection != BACKWARD) {
            analogWrite(pwmPin, 0); // force to disable
//LOG_COUT(info) << F("BACKWARD FORCE PWM TO 0 p1=") << in1Pin << F(" p2=") << in2Pin << F(" pwm=") << pwmPin << LOG_ENDLINE;
            delay(1);
            lastDirection = BACKWARD;
            desiredDirection = BACKWARD;
            digitalWrite(in1Pin, LOW);
            digitalWrite(in2Pin, HIGH);
        }
        analogWrite(pwmPin, desiredSpeed);
    }

    /*! \brief Reverse administratively-set direction.
     *
     * Takes effect on the next invocation of run() with
     * an UNCHANGED argument.
     *
     * \note Not compatible with original L298N class.
     *
     * \sa changeSpeed()
     */
    void reverseDirection()
    {
        desiredDirection = (desiredDirection == FORWARD) ?
                           BACKWARD : FORWARD;
    }

    /*! \brief Activate motor at previously set speed.
     *
     * \note The UNCHANGED argument is not compatible with the original
     * L298N class.
     * \sa setSpeed()
     */
    void run(Direction dir)
    {
        uint8_t useDirection = (dir != UNCHANGED) ?
                               dir : desiredDirection;
        useDirection ^= reversedPolarity;

        if (useDirection == BACKWARD) {
            backward();
        } else {
            forward();
        }
    }

    /*! \brief Change speed of active motor.
     *
     * \note Not compatible with original L298N class.
     * If \a newSpeed is 0, motor will be powered off.
     */
    void changeSpeed(uint8_t newSpeed, Direction newDir = UNCHANGED)
    {
        if (newSpeed == 0) { // full stop, power down
            digitalWrite(in1Pin, LOW);
            digitalWrite(in2Pin, LOW);
            desiredSpeed = newSpeed;
            analogWrite(pwmPin, 0);
            lastDirection = NEVER_SET;
        } else if ((desiredSpeed != 0) && // was not previously stopped
                   ((newDir == UNCHANGED) || (newDir == lastDirection))) {
            desiredSpeed = newSpeed;
            analogWrite(pwmPin, desiredSpeed);
        } else { // must change direction
            desiredSpeed = newSpeed;
            run(newDir);
        }
    }

    /*! \brief Immediately stop motor and power down.
     *
     * \note Not compatible with original L298N class.
     */
    void stopNowAndPowerDown()
    {
        changeSpeed(0);
    }

}; // end class SimpleL298N



/*! \brief Maintain state regarding one train.
 *
 * Combines motor speed and direction, presence sensor readings,
 * and sounds associated with train station arrival and
 * departures.
 */
template <uint8_t SENSORS_PER_TRACK> class TrainState : public EventClient
{
public:
    enum {
        MAX_TRAIN_EVENTS = 4, //<! Maximum pending events, should be power-of-2 to avoid division
        // PWM limits, 255=full speed, 0=stop
        SLOW_ENOUGH_TO_BE_STOPPED = 20, //!< Power low enough to stop engine
        SLOW_ENOUGH_TO_MOVE = 35, //!< Slowest initial power
        FASTEST_PWM_SPEED = 60, //!< Fastest speed permitted
        STARTING_PWM_POWER = 150 //!< Initial burst of starting power
    };

    enum MovementState {
        TRAIN_STOPPED = 0,
        _BACKWARD_BIT = 1, // for L298N class, FORWARD=0, BACKWARD=1
        _TRAIN_MOVING_BIT = 2,
        _CHANGE_SPEED_BIT = 4,
        _INCREASE_SPEED_BIT = 8,
        _REVERSE_DIRECTION_BIT = 16,
        _DELAY_BIT = 32,
        _INITIAL_START_BIT = 64,
        _PLAY_SOUND_BIT = 128,
        MAINTAIN_SPEED = _TRAIN_MOVING_BIT,
        INCREASE_FORWARD_SPEED = _INCREASE_SPEED_BIT | _CHANGE_SPEED_BIT | _TRAIN_MOVING_BIT,
        INCREASE_BACKWARD_SPEED = _BACKWARD_BIT | INCREASE_FORWARD_SPEED,
        DECREASE_FORWARD_SPEED = _CHANGE_SPEED_BIT | _TRAIN_MOVING_BIT,
        DECREASE_BACKWARD_SPEED = _BACKWARD_BIT | DECREASE_FORWARD_SPEED,
        START_FORWARD = INCREASE_FORWARD_SPEED | _INITIAL_START_BIT,
        START_BACKWARD = INCREASE_BACKWARD_SPEED | _INITIAL_START_BIT,
        STOP_TRAIN = _INITIAL_START_BIT,
        DELAY = _DELAY_BIT,
        PLAY_ARRIVAL = _PLAY_SOUND_BIT,
        PLAY_DEPARTURE = _PLAY_SOUND_BIT | _REVERSE_DIRECTION_BIT,
        WAIT_FOR_SOUND = _PLAY_SOUND_BIT | _DELAY_BIT,
        PLAY_AMBIENT_ARRIVAL = PLAY_ARRIVAL | _INITIAL_START_BIT,
        PLAY_AMBIENT_DEPARTURE = PLAY_AMBIENT_ARRIVAL | _REVERSE_DIRECTION_BIT,
        DELAY_THEN_INCREASE_FORWARD = _DELAY_BIT | INCREASE_FORWARD_SPEED,
        DELAY_THEN_INCREASE_BACKWARD = _DELAY_BIT | INCREASE_BACKWARD_SPEED
    };
    TrackSoundState     &soundStateObj;
    TrackSoundState     &ambientSoundObj;
public:
    TrackSensorStates<SENSORS_PER_TRACK>   trackSensors;
protected:
    /*! \brief Data structure to hold events for train movement 
     * state machine.
     */
    struct TrainEvent {
        ClockWithRollover eventStartTime;
        uint32_t          eventData;
        uint32_t          eventDuration;
        uint32_t          relativeToPrevious;
        uint8_t           eventType;
    } eventQueue[MAX_TRAIN_EVENTS];
    TrainEvent      &currentEvent;  // reference to current event

public:
    SimpleL298N         motor;
protected:
    ClockWithRollover   startOfEventMilliseconds;
    ClockWithRollover   endOfEventMilliseconds;
    ClockWithRollover   motorStartTime;
    const uint8_t       onTrackNumber;
    uint8_t             motorExpireAttempts;
    uint8_t             eventQueueHead;
    uint8_t             eventQueueTail;
    uint8_t             lastDirectionSet;
    uint8_t             lastStationDetected;

    /*! \brief Add event to movement queue.
     */
    void queueEvent(const ClockWithRollover now, uint8_t evType,
                    const ClockWithRollover startTime, uint32_t evData = 0,
                    uint32_t duration = 0, uint32_t relative = 0)
    {
        if (eventQueueHead == eventQueueTail) { // empty queue
#if LOG_ENABLED
            LOG_COUT(info) << F("queueEvent TrainState q was empty h=") << eventQueueHead << LOG_ENDLINE;
#endif
            if (startTime.isClockSet()) {
                startOfEventMilliseconds = startTime;
            } else {
                startOfEventMilliseconds = now;
            }
        }
#if LOG_ENABLED
        LOG_COUT(info) << F("QUEUE t=") << evType << F(" tail=") << eventQueueTail << F(" eventDur=") << duration << F(" rel=") << relative << LOG_ENDLINE;
#endif
        uint8_t nextTail = (eventQueueTail + 1) % MAX_TRAIN_EVENTS;
        eventQueue[eventQueueTail].eventStartTime = startTime;
        eventQueue[eventQueueTail].eventDuration = duration;
        eventQueue[eventQueueTail].relativeToPrevious = relative;
        eventQueue[eventQueueTail].eventType = evType;
        eventQueue[eventQueueTail].eventData = evData;
        eventQueueTail = nextTail;
    }

    /*! \brief Loads currentEvent as needed.
     */
    void processHeadEvent(const ClockWithRollover now, uint8_t headSubscript)
    {
#if LOG_ENABLED
        LOG_COUT(info) << F("TrainState::processHeadEvent ") << headSubscript <<
                       F(" type=") << eventQueue[headSubscript].eventType <<
                       F(" eventData=") << eventQueue[headSubscript].eventData << LOG_ENDLINE;
#endif

        if (eventQueue[headSubscript].eventStartTime.isClockSet()) {
            startOfEventMilliseconds = eventQueue[headSubscript].eventStartTime;
#if LOG_ENABLED
            LOG_COUT(info) << F("Set startTime from record ms=") << startOfEventMilliseconds.getIntervalMilliseconds() << LOG_ENDLINE;
#endif
        } else {
#if LOG_ENABLED
            LOG_COUT(info) << F("Set startTime to now + ") << eventQueue[headSubscript].relativeToPrevious << LOG_ENDLINE;
#endif
            startOfEventMilliseconds = now;
            startOfEventMilliseconds.addMilliseconds(eventQueue[headSubscript].relativeToPrevious);
        }
        endOfEventMilliseconds = startOfEventMilliseconds;
        endOfEventMilliseconds.addMilliseconds(eventQueue[headSubscript].eventDuration);

        uint8_t eventType = eventQueue[headSubscript].eventType;
        switch (eventType) {
        case DELAY:
        case DELAY_THEN_INCREASE_FORWARD:
        case DELAY_THEN_INCREASE_BACKWARD:
        case MAINTAIN_SPEED:
        case START_FORWARD:
        case START_BACKWARD:
        case INCREASE_FORWARD_SPEED:
        case INCREASE_BACKWARD_SPEED:
        case DECREASE_FORWARD_SPEED:
        case DECREASE_BACKWARD_SPEED:
        case STOP_TRAIN:
            currentEvent = eventQueue[headSubscript];
            break;
        case WAIT_FOR_SOUND:
#if LOG_ENABLED
            LOG_COUT(info) << F("WAIT_FOR_SOUND") << LOG_ENDLINE;
#endif
            break;
        case TRAIN_STOPPED:
        default:
#if LOG_ENABLED
            LOG_COUT(error) << F("UNEXPECTED t=") << eventType << LOG_ENDLINE;
#endif
            break;
        } // end switch eventType
    } // end processHeadEvent

    bool popEvent(const ClockWithRollover now)
    {
        if (eventQueueHead == eventQueueTail) {
#if LOG_ENABLED
            LOG_COUT(warn) << F("TrainState queue empty") << LOG_ENDLINE;
#endif
            startOfEventMilliseconds.resetClock();
            endOfEventMilliseconds.resetClock();
            return (false); // empty queue
        }
        uint8_t nextHead = (eventQueueHead + 1) % MAX_TRAIN_EVENTS;
        uint8_t curHead = eventQueueHead;
#if LOG_ENABLED
        if (eventQueue[curHead].eventStartTime.isClockSet()) {
            LOG_COUT(info) << F("time til: ") << eventQueue[curHead].eventStartTime - now << F(" e=") << eventQueue[curHead].eventStartTime.getIntervalMilliseconds() << F(" n=") << now.getIntervalMilliseconds() << LOG_ENDLINE;
        }
#endif
#if LOG_ENABLED
        LOG_COUT(info) << F("Start time reached head=") << curHead <<
                       F(" tail=") << eventQueueTail <<
                       F(" t=") << eventQueue[curHead].eventType << LOG_ENDLINE;
#endif
        eventQueueHead = nextHead; // pop the event
        processHeadEvent(now, curHead); // but allow it to be used
        return (true); // did something
    } // end popEvent()

    void stopMotor()
    {
        motor.stopNowAndPowerDown();
    }

public:
    TrainState(uint8_t trackNumber,
               TrackSoundState &soundState,
               TrackSoundState &ambient,
               const uint8_t sensorPins[SENSORS_PER_TRACK],
               const uint8_t motorEnablePin,
               const uint8_t motorInA, const uint8_t motorInB) :
        soundStateObj(soundState),
        ambientSoundObj(ambient),
        trackSensors(trackNumber, sensorPins),
        currentEvent(eventQueue[0]),
        motor(motorEnablePin, motorInA, motorInB),
        onTrackNumber(trackNumber)
    {
        eventQueueHead = 0;
        eventQueueTail = 0;
        motorExpireAttempts = 1;
        currentEvent.eventType = TRAIN_STOPPED;
        lastDirectionSet = 0;
        lastStationDetected = ~0;
        stopMotor();
    }

    virtual ~TrainState() {}

    /*! \brief Return an indication if the track has been enable for use
     */
    bool isTrackEnabled() const
    {
        return (trackSensors.isTrackActive());
    }

    /*! \brief Set the number of attempts to power on a motor
     * that can be made without any train progress being detected.
     *
     * A train may fail to make progress between senors for a variety
     * of means, inluding already being at one of the track.  A reversal
     * can be automatically attempted before the system will cease
     * further attempts.
     */
    void setMotorExpireAttempts(uint8_t n)
    {
        motorExpireAttempts = n;
    }

    /*! \brief Tie ambient and station sound state objects together.
     *
     * \param stationMode specifies how the station arrival sounds
     * should be cycled.  The default is to progress from station to
     * station and then start a new cycle of alternate sounds.
     * \param endpointMode specifies how the ambient sounds should be
     * cycled.  The default is to not adjust the current station
     * number and only rotate among station-specific alternate sounds.
     */
    void synchronizeAmbientSounds(TrackSoundState::eSoundCycleDirection stationMode = TrackSoundState::NEXT_STATION_THEN_NEW_CYCLE, TrackSoundState::eSoundCycleDirection endpointMode = TrackSoundState::NEW_CYCLE_ONLY)
    {
        soundStateObj.setCycleMode(stationMode);
        ambientSoundObj.synchronizeStationsWith(soundStateObj, endpointMode);
    }

    /*! \brief Discard all pending events.
     */
    void clearQueue()
    {
        eventQueueHead = 0;
        eventQueueTail = 0;
        startOfEventMilliseconds.resetClock();
        endOfEventMilliseconds.resetClock();
    }


    /*! \brief Queue movement request
    */
    void setMovementDirection(const ClockWithRollover now,
                              MovementState newState,
                              const ClockWithRollover startTime,
                              uint32_t relativeToPrior = 0,
                              uint32_t duration = 0,
                              uint32_t evData = 0)
    {
#if LOG_ENABLED && DEBUG_TRACK_POWER
        LOG_COUT(info) << F(" track=") << onTrackNumber <<
                       F(" Set movement state=") << newState <<
                       F(" dur=") << duration << F(" rel=") << relativeToPrior << LOG_ENDLINE;
#endif
        queueEvent(now, newState, startTime, evData, duration, relativeToPrior);
    }

    bool playTrainArrived(const ClockWithRollover now, uint32_t soundData = 0, TrackSoundState::eSoundCycleDirection dir = TrackSoundState::USE_DEFAULT)
    {
        TrackSoundState::SoundID sound = soundStateObj.getNextStationSound(dir);
        bool ok = soundController.queueSoundRequest(now, sound.folderNumber, sound.fileNumber, this, soundData, false);
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("playTrainArrived alwaysFalse ok=") << ok << LOG_ENDLINE;
#endif
        return (ok);
    }

    bool playAmbientSound(const ClockWithRollover now, uint32_t soundData = 0, TrackSoundState::eSoundCycleDirection dir = TrackSoundState::USE_DEFAULT)
    {
        TrackSoundState::SoundID sound = ambientSoundObj.getNextStationSound(dir);
        bool ok = soundController.queueSoundRequest(now, sound.folderNumber, sound.fileNumber, this, soundData, false);
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("playAmbient ok=") << ok << LOG_ENDLINE;
#endif
        return (ok);
    }

    uint8_t waitForSound(const ClockWithRollover now, uint32_t delay)
    {
        ClockWithRollover timeNotSet;
        uint32_t evData = (DELAY_THEN_INCREASE_FORWARD | lastDirectionSet);
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("waitForSound delay ") << delay <<
                       F(" tail=") << eventQueueTail << LOG_ENDLINE;
#endif
        uint8_t result = eventQueueTail;
        queueEvent(now, WAIT_FOR_SOUND, timeNotSet, evData, static_cast<uint32_t>(MAX_SOUND_LENGTH_SECONDS) * 1000, delay);
        return (result);
    }

    void increaseSpeed(const ClockWithRollover now, uint32_t interval = SPEED_UP_INTERVAL_MS, uint32_t relative = 0)
    {
        ClockWithRollover timeNotSet;

        // only increase speed, do not change current direction
        setMovementDirection(now, static_cast<MovementState>(START_FORWARD | lastDirectionSet),
                             timeNotSet, relative, interval);
    }

    /*! \brief Queue commands to slow train to a stop.
     *
     * \retval true indicates train already brought to a stop.
     * \retval false indicates command to slow train queued.
     */
    bool slowAndStop(const ClockWithRollover now, uint32_t withinMS = SLOW_DOWN_INTERVAL_MS)
    {
        ClockWithRollover timeNotSet;

        uint16_t currentSpeed = motor.getSpeed();
        if (currentSpeed <= (SLOW_ENOUGH_TO_BE_STOPPED)) { // already reached stop state
            if (currentEvent.eventType != TRAIN_STOPPED) {
#if LOG_ENABLED && DEBUG_TRACK_POWER
                LOG_COUT(info) << F(" track=") << onTrackNumber << F(" slowAndStop -- effectively stopped") << LOG_ENDLINE;
#endif
                setMovementDirection(now, STOP_TRAIN, timeNotSet, 0, withinMS);
            }
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F(" track=") << onTrackNumber << F(" slowAndStop -- already stopped speed=") << currentSpeed << LOG_ENDLINE;
#endif
            return (true);
        }
        // NOTE: currentSpeed must be non-zero to get here..
        uint32_t stepDelay = withinMS / currentSpeed;
#if LOG_ENABLED && DEBUG_TRACK_POWER
        LOG_COUT(info) << F(" track=") << onTrackNumber <<
                       F(" slowAndStop step=") << stepDelay <<
                       F(" interval=") << withinMS << LOG_ENDLINE;
#endif
        switch (currentEvent.eventType) {
        case TRAIN_STOPPED:
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" slowAndStop -- already stopped") << LOG_ENDLINE;
#endif
            return (true);
        case STOP_TRAIN:
#if LOG_ENABLED
            LOG_COUT(info) << F("STOP_TRAIN request already queued") << LOG_ENDLINE;
#endif
        // fallthrough
        case MAINTAIN_SPEED:
        case INCREASE_FORWARD_SPEED:
        case INCREASE_BACKWARD_SPEED:
#if LOG_ENABLED
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" slowAndStop speed change was increasing") << LOG_ENDLINE;
#endif
            // convert to DECREASE speed, but retain direction
            setMovementDirection(now,
                                 static_cast<MovementState>(DECREASE_FORWARD_SPEED | lastDirectionSet),
                                 timeNotSet, 0, withinMS, stepDelay);
            break;
        // TODO:  could combine with INCREASE case.
        case DECREASE_FORWARD_SPEED:
        case DECREASE_BACKWARD_SPEED:
#if LOG_ENABLED
            LOG_COUT(info) << F(" track=") << onTrackNumber << F(" slowAndStop speed change was decreasing") << LOG_ENDLINE;
#endif
            // force stop within delay period
            setMovementDirection(now, static_cast<MovementState>(currentEvent.eventType),
                                 timeNotSet, 0, withinMS, stepDelay);
            break;
        default: // should never happen
#if LOG_ENABLED
            LOG_COUT(error) << F(" track=") << onTrackNumber << F(" Movement state not supported ") << currentEvent.eventType << LOG_ENDLINE;
#endif
            setMovementDirection(now, STOP_TRAIN, now);
            break;
        } // end switch eventType
        return (false);
    } // end slowAndStop()

    /*! \brief Process state machine for train movement.
     *
     * \retval true indicates end of event was reached
     */
    bool processTrainMovement(const ClockWithRollover now)
    {
        // NOTE:  this routine is called a LOT
        if (motorStartTime.isClockSet()) {
            int32_t msDiff = now - motorStartTime;
            if (msDiff >= MAX_MOTOR_RUNTIME_MS) {
#if LOG_ENABLED && DEBUG_TRACK_POWER
                LOG_COUT(warn) << F("Motor exceeded max runtime ms=") << msDiff <<
                               F(" limit=") << MAX_MOTOR_RUNTIME_MS << LOG_ENDLINE;
#endif
                stopMotor();
                clearQueue(); // forget any pending events
                motorStartTime.resetClock();
                if (motorExpireAttempts == 0) {
                    currentEvent.eventType = TRAIN_STOPPED;
#if LOG_ENABLED
                    LOG_COUT(error) << F("CEASE")  << LOG_ENDLINE;
#endif
#if ENABLE_LED_BLINK_NOTIFICATIONS
                    LEDalarm.setFlashes(sizeof(alarmFlashes_noProgress), alarmFlashes_noProgress);
                    endOfActiveCycle.resetClock();
#endif
                } else {
#if LOG_ENABLED
                    LOG_COUT(warn) << F("NO PROGRESS, reverse dir") << LOG_ENDLINE;
#endif
                    beginNewTrainCycle(now, lastDirectionSet);
                    motorExpireAttempts -= 1;
                }
            }
        }
        bool result = false;
        if (startOfEventMilliseconds.isClockSet()) {
            if (!(startOfEventMilliseconds < now)) { // have not reached start of event
                return (false);
            }
            if (!(now < endOfEventMilliseconds)) { // reached end of event
#if LOG_ENABLED && (DEBUG_TRACK_POWER > 0)
                LOG_COUT(info) << F("reached end of event ") << eventQueueHead << F(" curType=") << currentEvent.eventType << F(" call popEvent") << LOG_ENDLINE;
#endif
                // pop event
                result = popEvent(now);

                if (startOfEventMilliseconds.isClockSet()) {
#if LOG_ENABLED
                    LOG_COUT(info) << F("Time till next=") << startOfEventMilliseconds - now << LOG_ENDLINE;
#endif

                    if (!(startOfEventMilliseconds < now)) { // have not reached start of event
#if LOG_ENABLED
                        LOG_COUT(info) << F("Waiting for new event to start in ") << startOfEventMilliseconds - now << LOG_ENDLINE;
#endif
                        return (false);
                    }
                }
            } // end reached end of event
        } // end if startOfEventMilliseconds was set
#if LOG_ENABLED && (DEBUG_TRACK_POWER > 2)
        LOG_COUT(info) << F(" track=") << onTrackNumber <<
                       F(" processTrainMovement movementState=") << currentEvent.eventType << LOG_ENDLINE;
#endif
        switch (currentEvent.eventType) {
        case TRAIN_STOPPED:
        default: // just in case
            //          stopMotor();
            break;
        case MAINTAIN_SPEED:
            // do nothing
            break;
        case DELAY: {
            currentEvent.eventType = static_cast<uint8_t>(currentEvent.eventData);
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F("Convert DELAY to ") << currentEvent.eventType << LOG_ENDLINE;
#endif
        }
        break;
        case DELAY_THEN_INCREASE_FORWARD:
        case DELAY_THEN_INCREASE_BACKWARD: {
            uint8_t dir = currentEvent.eventType & _BACKWARD_BIT;
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << "DELAY_THEN_INCREASE dir=" << dir << LOG_ENDLINE;
#endif
            currentEvent.eventType = START_FORWARD | dir;
        }
        break;
        case STOP_TRAIN:
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F("STOP TRAIN") << LOG_ENDLINE;
#endif
            stopMotor();
            currentEvent.eventType = TRAIN_STOPPED;
            break;
        case START_FORWARD:
        case START_BACKWARD: {
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" START movementState=") << currentEvent.eventType <<

                           F(" dir=") <<
                           ((currentEvent.eventType & 1) ? F("backward") : F("forward")) <<
                           F(" arg=") << currentEvent.eventData <<
                           F(" duration=") << currentEvent.eventDuration <<
                           F(" relative=") << currentEvent.relativeToPrevious << LOG_ENDLINE;
#endif
            lastDirectionSet = currentEvent.eventType & _BACKWARD_BIT;
            motorStartTime = now;
            motor.setSpeed(STARTING_PWM_POWER); // use burst of higher power level to get train moving
            motor.run(static_cast<SimpleL298N::Direction>(currentEvent.eventType & 1));
            motor.setSpeed(SLOW_ENOUGH_TO_MOVE); // continue on with lower power
            currentEvent.eventType = currentEvent.eventType & ~(_INITIAL_START_BIT); // drop initial start condition
#if ANALOG_DIAL_CONTROLS_SPEED == 1
            // analog dial is used to control max train speed
            // For precision, dial is used to scale range between
            // [SLOW_ENUGH_TO_STOPPED, FASTEST_PWM_SPEED)
            const uint16_t maxSpeed  = ((analogDial.getValue() * (FASTEST_PWM_SPEED - SLOW_ENOUGH_TO_BE_STOPPED)) / analogDial.ANALOG_VALUE_RANGE) + SLOW_ENOUGH_TO_BE_STOPPED;
#else
            const uint16_t maxSpeed = FASTEST_PWM_SPEED;
#endif
            int16_t changeNeeded = maxSpeed - SLOW_ENOUGH_TO_MOVE;
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F("change=") << changeNeeded << F(" max=") << maxSpeed << LOG_ENDLINE;
#endif
            if (changeNeeded > 0) {
                // interval in units of 1/100ths of a second
                uint32_t stepDelay = currentEvent.eventDuration / (10 * changeNeeded);
                if ((stepDelay == 0) && (currentEvent.eventDuration != 0)) {
                    stepDelay = 1; // round up
                }
                currentEvent.eventData = stepDelay;
            }
        }
        break;
        case INCREASE_FORWARD_SPEED:
        case INCREASE_BACKWARD_SPEED: {
            uint16_t currentSpeed = motor.getSpeed();
#if ANALOG_DIAL_CONTROLS_SPEED == 1
            // analog dial is used to control max train speed
            // For precision, dial is used to scale range between
            // [SLOW_ENUGH_TO_STOPPED, FASTEST_PWM_SPEED)
            const uint16_t maxSpeed  = ((analogDial.getValue() * (FASTEST_PWM_SPEED - SLOW_ENOUGH_TO_BE_STOPPED)) / analogDial.ANALOG_VALUE_RANGE) + SLOW_ENOUGH_TO_BE_STOPPED;
#else
            const uint16_t maxSpeed = FASTEST_PWM_SPEED;
#endif
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" increase ") <<
                           ((currentEvent.eventType & 1) ? F("backward") : F("forward")) <<
                           F(" speed=") << currentSpeed <<
                           F(" max=") << maxSpeed <<
                           F(" interval=") << currentEvent.eventData << LOG_ENDLINE;
#endif
            if (currentSpeed < maxSpeed) {
                motor.setSpeed(currentSpeed + 1);
                // L298N class has FORWARD=0, BACKWARD=1
                lastDirectionSet = currentEvent.eventType & 1;
                motor.run(static_cast<SimpleL298N::Direction>(lastDirectionSet));
                uint32_t ms = currentEvent.eventData * 10;
                startOfEventMilliseconds = now;
                startOfEventMilliseconds.addMilliseconds(ms); // argument in units of 1/100th seconds
                endOfEventMilliseconds = startOfEventMilliseconds;
                endOfEventMilliseconds.addMilliseconds(currentEvent.eventDuration);
            } else {
#if LOG_ENABLED && DEBUG_TRACK_POWER
                LOG_COUT(info) << F("max speed reached, maintain") <<
                               F(" dir=") << (currentEvent.eventType & 1) << LOG_ENDLINE;
#endif
                currentEvent.eventType = MAINTAIN_SPEED;
                endOfEventMilliseconds = now; // end of event reached
            }
        }
        break;
        case DECREASE_FORWARD_SPEED:
        case DECREASE_BACKWARD_SPEED: {
            uint16_t currentSpeed = motor.getSpeed();
#if LOG_ENABLED && DEBUG_TRACK_POWER
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" decrease ") <<
                           ((currentEvent.eventType & 1) ? F("backward") : F("forward")) <<
                           F(" speed=") << currentSpeed <<
                           F(" interval=") << currentEvent.eventData << LOG_ENDLINE;
#endif
            if (currentSpeed > SLOW_ENOUGH_TO_BE_STOPPED) {
                motor.setSpeed(currentSpeed - 1);
                // L298N class has FORWARD=0, BACKWARD=1
                motor.run(static_cast<SimpleL298N::Direction>(currentEvent.eventType & 1));

                uint32_t ms = currentEvent.eventData * 10;
//              LOG_COUT(info) << F("add ") << ms << F(" milliseconds to start") << LOG_ENDLINE;
                startOfEventMilliseconds = now;
                startOfEventMilliseconds.addMilliseconds(ms); // argument in units of 1/100th second
                endOfEventMilliseconds = startOfEventMilliseconds;
                endOfEventMilliseconds.addMilliseconds(currentEvent.eventDuration);
            } else {
#if LOG_ENABLED
                LOG_COUT(info) << F("consider stopped") << LOG_ENDLINE;
#endif
                stopMotor();
                motorStartTime.resetClock();
                currentEvent.eventType = TRAIN_STOPPED;
                endOfEventMilliseconds = now;
            }
        }
        break;
        } // end switch currentEvent.eventType
        return (result);
    } // end processTrainMovement()


    /*! \brief Process change in Hall effect sensor used to detect
     * train presence.
     *
     * If a train has arrived at a station, play an appropriate sound
     * while slowing it to a stop, delay, then proceed on to the next
     * station.  End point stations force change of direction.
     */
    void processSensorChange(const ClockWithRollover now, uint8_t sensorSubscript, uint16_t sensorReading)
    {
        uint16_t changeType = TrackSensorStates<MAX_SENSORS_PER_TRACK>::extractSensorChangeFlags(sensorReading, sensorSubscript);
#if LOG_ENABLED && (DEBUG_TRACK_SENSORS > 1)
        LOG_COUT(info) << F(" track=") << onTrackNumber <<
                       F(" subscript=") << sensorSubscript <<
                       F(" reading=") << sensorReading <<
                       F(" changeType=") << changeType << LOG_ENDLINE;
#endif
        switch (changeType) {
        case TrackSensorStates<SENSORS_PER_TRACK>::TRAIN_ARRIVED_FLAG: {
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
            LOG_COUT(info) << F(" track=") << onTrackNumber <<
                           F(" Train Arrived at ") << sensorSubscript << LOG_ENDLINE;
#endif
            if (lastStationDetected != sensorSubscript) {
                lastStationDetected = sensorSubscript;
                clearQueue(); // reset everything when arrived at a station/endpoint
                slowAndStop(now, SLOW_DOWN_INTERVAL_MS);
                motorExpireAttempts = 1; // reset since arrival proven
                uint32_t waitTime = (sensorSubscript != 0) ?
                    random(MIN_END_STATION_WAIT_TIME_MS, MAX_END_STATION_WAIT_TIME_MS) :
                    random(MIN_MID_STATION_WAIT_TIME_MS, MAX_MID_STATION_WAIT_TIME_MS);
                uint8_t eventId = waitForSound(now, static_cast<uint32_t>(MAX_SOUND_LENGTH_SECONDS) * 1000);
                switch (sensorSubscript) {
                case 1: // at left end
                case 2: { // at right end
#if LOG_ENABLED
                    LOG_COUT(info) << F("PLAY AMBIENT") << LOG_ENDLINE;
#endif
                    bool ok = playAmbientSound(now, eventId + 1);
                    // set direction
                    lastDirectionSet = (sensorSubscript == 1) ? 0 : _BACKWARD_BIT;
                }
                break;
                case 0: // at station
                default: {
#if LOG_ENABLED
                    LOG_COUT(info) << F("PLAY ARRIVED") << LOG_ENDLINE;
#endif
                    bool ok = playTrainArrived(now, eventId + 1);
                }
                    // retain current direction
                break;
                } // end switch sensorSubscript
                increaseSpeed(now, SPEED_UP_INTERVAL_MS, waitTime);
            }
        }
        break;
        default:
            break;
        } // end switch changeType
    } // end processSensorChange

    /*! \brief Process notification that a requested sound has
     * finished playing.
     *
     * \sa TrainSoundController
     */
    virtual void handleEvent(const ClockWithRollover now, uint8_t ok, uint32_t data)
    {
        // handle requested sound finished
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
        LOG_COUT(info) << F("====TrainState::handleEvent") <<
                       F(" ok=") << ok <<
                       F(" data=") << data <<
                       F(" track=") << onTrackNumber << LOG_ENDLINE;
#endif
        if (ok != 1) { // not in progress
            if (data != 0) { // value was set
                uint8_t subscript = static_cast<uint8_t>(data) - 1;
                uint8_t evType = eventQueue[subscript].eventType;
                if ((evType & _PLAY_SOUND_BIT) != 0) {
#if LOG_ENABLED
                    LOG_COUT(info) << F("PLAY SOUND event at ") << subscript << F(" delay=") << eventQueue[subscript].relativeToPrevious << F(" head=") << eventQueueHead << LOG_ENDLINE;
#endif
                    // drop PLAY_SOUND_BIT
                    eventQueue[subscript].eventType = evType & ~_PLAY_SOUND_BIT;
                    eventQueue[subscript].eventStartTime = now;
                    eventQueue[subscript].eventDuration = eventQueue[subscript].relativeToPrevious;
                    eventQueue[subscript].relativeToPrevious = 0;
//                eventQueue[subscript].eventStartTime.addMilliseconds(eventQueue[subscript].relativeToPrevious);
                    uint8_t nextSub = (subscript + 1) % MAX_TRAIN_EVENTS;

                    if (nextSub == eventQueueHead) { // was event in progress
#if LOG_ENABLED
                        LOG_COUT(info) << F("set endOfEvent") << LOG_ENDLINE;
#endif
                        endOfEventMilliseconds = now;
                        endOfEventMilliseconds.addMilliseconds(eventQueue[subscript].eventDuration);
                        popEvent(now);
                    }
                } else {
#if LOG_ENABLED
                    LOG_COUT(info) << F("evType was ") << evType << LOG_ENDLINE;
#endif
                }
            } else {
#if LOG_ENABLED
                LOG_COUT(info) << F("sub not set") << LOG_ENDLINE;
#endif
            }
        }
    }

    void beginNewTrainCycle(const ClockWithRollover now, uint8_t dir = TrainState::_BACKWARD_BIT)
    {
        stopMotor();
        clearQueue();
        lastStationDetected = ~0;

        startOfActiveCycle = now;
        updateCycleEndTime();

        uint8_t flippedDir = (dir ^ _BACKWARD_BIT) & _BACKWARD_BIT;
#if LOG_ENABLED
        LOG_COUT(info) << F(" flippedDir=") << flippedDir << LOG_ENDLINE;
#endif
        uint32_t intervalArg  = static_cast<uint32_t>(SPEED_UP_INTERVAL_MS);
        setMovementDirection(now, static_cast<TrainState::MovementState>(TrainState::START_FORWARD | flippedDir), now, 0, SPEED_UP_INTERVAL_MS,
                             intervalArg);
    }

    // start a new train cycle if system not currently busy
    void activateTrainCycle(const ClockWithRollover now)
    {
        if (endOfActiveCycle <= now) {
#if LOG_ENABLED
            LOG_COUT(info) << F("activateTrainCycle") << LOG_ENDLINE;
#endif
            motorExpireAttempts = 1;
            beginNewTrainCycle(now, currentEvent.eventType);
        }
    }

}; // end class TrainState


/*! \brief Optionally store track number in TrackSoundState objects.
 */
#define STORE_TRACK_NUMBER_IN_SOUND_STATE 0

static TrackSoundState trackSounds[] = {
    {
        0,
        YAMANOTE_LINE_STATION_TOTAL, YAMANOTE_LINE_SOUNDS_PER_STATION,
        YAMANOTE_LINE_FOLDER_NUMBER, YAMANOTE_LINE_BASE_SOUND
    },
    {
        1,
        SHINKANSEN_STATION_TOTAL, SHINKANSEN_SOUNDS_PER_STATION,
        SHINKANSEN_FOLDER_NUMBER, SHINKANSEN_BASE_SOUND
    }
};

static TrackSoundState ambientSounds[] = {
    {
        0,
        YAMANOTE_AMBIENT_STATION_TOTAL, YAMANOTE_AMBIENT_SOUNDS_PER_STATION,
        YAMANOTE_AMBIENT_FOLDER_NUMBER, YAMANOTE_AMBIENT_BASE_SOUND
    },
    {
        1,
        SHINKANSEN_AMBIENT_STATION_TOTAL, SHINKANSEN_AMBIENT_SOUNDS_PER_STATION,
        SHINKANSEN_AMBIENT_FOLDER_NUMBER, SHINKANSEN_AMBIENT_BASE_SOUND
    }
};

// First pin is station presence pin, others are end-of-track
static const uint8_t trackSensorPins[][MAX_SENSORS_PER_TRACK] = {
    { PIN_TRACK1_STATION_SENSOR, PIN_TRACK1_END_A_SENSOR, PIN_TRACK1_END_B_SENSOR },
    { PIN_TRACK2_STATION_SENSOR, PIN_TRACK2_END_A_SENSOR, PIN_TRACK2_END_B_SENSOR }
};

static TrainState<MAX_SENSORS_PER_TRACK> trainState[] = {
    {
        0, trackSounds[0], ambientSounds[0], trackSensorPins[0],
        PIN_TRACK1_MOTOR_SPEED, PIN_TRACK1_DIR0, PIN_TRACK1_DIR1
    },
    {
        1, trackSounds[1], ambientSounds[1], trackSensorPins[1],
        PIN_TRACK2_MOTOR_SPEED, PIN_TRACK2_DIR0, PIN_TRACK2_DIR1
    }
};

static bool trackEnabled[MAX_TRAIN_TRACKS];

static void resetAllTracks(const ClockWithRollover now)
{
    uint8_t dirToggle = 0;
    for (uint8_t i = 0; i < MAX_TRAIN_TRACKS; i += 1) {
        dirToggle = 1 - dirToggle;
        trainState[i].setMotorExpireAttempts(1);
        trainState[i].beginNewTrainCycle(now, (i == 1) ? TrainState<MAX_SENSORS_PER_TRACK>::START_FORWARD : TrainState<MAX_SENSORS_PER_TRACK>::START_BACKWARD);
    }
}

#if 0
/* NOTE: for now, these interrupt routines are minimzed to
    just read the current state and the actual processing is
    deferred to the loop() routine.
*/
static void interrupt0Routine()
{
    soundController.lastSoundStateRead = digitalRead(PIN_INTERRUPT0);
}

static void interrupt1Routine()
{
    uint8_t currentLevel = digitalRead(PIN_INTERRUPT1);
    lastActivateModeRead = (lastActivateModeRead & ~1) + 2;
    if (currentLevel == HIGH) {
        lastActivateModeRead |= 1;
    }
}
#endif



#if (DUMP_STATE_INTERVAL_SECONDS != 0) && LOG_ENABLED
static void dumpDebugState(uint32_t currentTime, uint32_t lastInterval)
{
    uint8_t currentLevel = digitalRead(PIN_INTERRUPT1);
    LOG_COUT(debug) << F("lastActivateModeSet=") << motionTrigger.lastActivateModeSet << F(" level=") << currentLevel << LOG_ENDLINE;
}
#endif // end DUMP_STATE_INTERVAL_SECONDS != 0


static void processConsoleCommand(const ClockWithRollover now, const char *cmd)
{
#if LOG_ENABLED
    LOG_COUT(debug) << F("cmd=") << cmd << LOG_ENDLINE;
#endif
    uint16_t val;
    if (strcmp(cmd, "1+") == 0) { // speed up track 1
        val = trainState[0].motor.getSpeed();
        if (val > 0) {
            val -= 1;
            trainState[0].motor.setSpeed(val);
        }
    } else if (strcmp(cmd, "1-") == 0) { // slow down track 1
        val = trainState[0].motor.getSpeed();
        if (val < 255) {
            val += 1;
            trainState[0].motor.setSpeed(val);
        }
    } else if (strcmp(cmd, "2+") == 0) { // speed up track 2
        val = trainState[1].motor.getSpeed();
        if (val > 0) {
            val -= 1;
            trainState[1].motor.setSpeed(val);
        }
    } else if (strcmp(cmd, "2-") == 0) { // slow down track 2
        val = trainState[1].motor.getSpeed();
        if (val < 255) {
            val += 1;
            trainState[1].motor.setSpeed(val);
        }
    }
}

#if ENABLE_CONSOLE_COMMANDS > 0
static uint8_t consoleReadOffset;
static char consoleCmdBfr[64];

static void readAndProcessConsoleCommands(const ClockWithRollover now)
{
    if (Serial.available() == false) {
        return; // nothing available
    }

    uint8_t consoleReadOffset = 0;
    while (consoleReadOffset < (sizeof(consoleCmdBfr) - 1)) {
        uint8_t c = Serial.read();
        if ((c == '\r') || (c == '\n')) {
            break;
        }
        consoleCmdBfr[consoleReadOffset++] = c;
    }
    //    Serial.stopListening();
    if (consoleReadOffset == 0) { // null string
        return;
    }
    consoleCmdBfr[consoleReadOffset] = '\0';
    consoleReadOffset = 0; // reset
    processConsoleCommand(now, consoleCmdBfr);
}
#endif

// used to handle press-and-hold of button or motion trigger
static void processLongDurationPress(const ClockWithRollover now)
{
    uint32_t currentTime = now.getIntervalMilliseconds();
    uint32_t timeDiff = currentTime - motionTrigger.lastActivateModeTime;
#if LOG_ENABLED && DEBUG_MOTION_SENSOR
    LOG_COUT(info) << F("ltimeDiff=") << timeDiff << LOG_ENDLINE;
#endif
    if (timeDiff > (static_cast<uint32_t>(MIN_SECONDS_BETWEEN_CYCLES) * 1000)) { // enough time since last activation
        LEDalarm.turnOff();
        motionTrigger.lastActivateModeTime = currentTime;
        for (uint8_t track = 0; track < MAX_TRAIN_TRACKS; track += 1) {
            trainState[track].activateTrainCycle(now);
        }
    }
}

// Used to handle quick button press
static void processShortDurationPress(const ClockWithRollover now)
{
    uint32_t currentTime = now.getIntervalMilliseconds();
    uint32_t timeDiff = currentTime - motionTrigger.lastActivateModeTime;
#if LOG_ENABLED
    LOG_COUT(info) << F("stimeDiff=") << timeDiff << LOG_ENDLINE;
#endif
    if (timeDiff > (static_cast<uint32_t>(MIN_SECONDS_BETWEEN_CYCLES) * 1000)) { // enough time since last activation
        LEDalarm.turnOff();
        motionTrigger.lastActivateModeTime = currentTime;
#if LOG_ENABLED
        LOG_COUT(info) << F("processShortDurationPress ") << motionTrigger.lastActivateModeRead <<
                       F(" lastActivateModeSet=") << motionTrigger.lastActivateModeSet << LOG_ENDLINE;
#endif
//    processConsoleCommand(currentTime, "1+");
        resetAllTracks(now);
    }
}

/*! \brief Check if a track is enabled for operation.
 *
 *  \return A bit mask indicating if the selected track
 *  is enabled for operation.
 */
static uint8_t checkIfTrackEnabled(uint8_t track)
{
    uint8_t result = 0;
    bool enabled = trainState[track].isTrackEnabled();
    if (enabled) {
        result |= 1 << track;
    }
    if (enabled != trackEnabled[track]) { // state change
        trackEnabled[track] = enabled;
        result |= 1 << (MAX_TRAIN_TRACKS + track);
#if LOG_ENABLED
        LOG_COUT(info) << F("track ") << track << F(" enabled=") << trackEnabled[track] << LOG_ENDLINE;
#endif
    }
    return (result);
}

/*! \brief Scan track sensors to detect train movement,
 * adjust motor speeds as needed.
 */
static void monitorTrainMovement(const ClockWithRollover now, uint8_t track)
{
    uint8_t enabled = checkIfTrackEnabled(track);
    if ((enabled & (1 << (MAX_TRAIN_TRACKS + track))) != 0) { // state change
        if ((enabled & (1 << track)) == 0) { // not enabled anymore
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
            LOG_COUT(change) << F("track=") << track << F(" now disabled") << LOG_ENDLINE;
#endif
            trainState[track].clearQueue();
            trainState[track].slowAndStop(now, SLOW_DOWN_INTERVAL_MS);
            return;
        }
#if LOG_ENABLED && DEBUG_TRACK_SENSORS
        LOG_COUT(change) << F("track=") << track << F(" now enabled") << LOG_ENDLINE;
#endif
    } else { // no state change
        if ((enabled & (1 << track)) == 0) { // not enabled
            return;
        }
    }

    // LOG_COUT(info) << F("Loop track=") << track << LOG_ENDLINE;
    uint16_t sensorReading = trainState[track].trackSensors.readAllSensors();
    if (sensorReading != 0) { // some sensor changed
        uint8_t sensorSubscript = TrackSensorStates<MAX_SENSORS_PER_TRACK>::extractFirstSensorSubscript(sensorReading);
        while (sensorSubscript != MAX_SENSORS_PER_TRACK) {
            uint16_t testMask = TrackSensorStates<MAX_SENSORS_PER_TRACK>::sensorChangeMask(sensorSubscript,
                                static_cast<TrackSensorStates<MAX_SENSORS_PER_TRACK>::eChangeFlags>(TrackSensorStates<MAX_SENSORS_PER_TRACK>::TRAIN_ARRIVED_FLAG | TrackSensorStates<MAX_SENSORS_PER_TRACK>::TRAIN_DEPARTED_FLAG),
                                (1 << sensorSubscript));

#if LOG_ENABLED && DEBUG_TRACK_SENSORS
            LOG_COUT(info) << F("track ") << track << F(" subscript=") << sensorSubscript <<
                           F(" sensor reading changed=") << sensorReading << LOG_ENDLINE;
#endif
            trainState[track].processSensorChange(now, sensorSubscript, sensorReading);
            sensorReading &= ~(testMask);
            sensorSubscript = TrackSensorStates<MAX_SENSORS_PER_TRACK>::extractFirstSensorSubscript(sensorReading);
        } // end while sensors to process
    } else {
        trainState[track].processTrainMovement(now);
    }
}

#if TEST_MOTOR_SPEED
static uint32_t lastCheckTime;
static int16_t powerLevel = TrainState::SLOW_ENOUGH_TO_BE_STOPPED;
static uint8_t levelDir;

static void changeMotor(uint32_t ms)
{
    uint32_t diff = ms - lastCheckTime;
    if (diff < 1000) {
        return;
    }
    lastCheckTime = ms;
    if (levelDir == 0) {
        if (powerLevel >= TrainState::FASTEST_PWM_SPEED) {
            levelDir = 1;
        } else {
            powerLevel += 1;
            trainState[0].motor.setSpeed(powerLevel);
            trainState[1].motor.setSpeed(powerLevel);

        }
    } else {
        if (powerLevel <= TrainState::SLOW_ENOUGH_TO_BE_STOPPED) {
            levelDir = 0;
        } else {
            powerLevel -= 1;
            trainState[0].motor.setSpeed(powerLevel);
            trainState[1].motor.setSpeed(powerLevel);
        }
    }
    LOG_COUT(info) << F("dir=") << levelDir << F(" pwr=") << powerLevel << LOG_ENDLINE;
    trainState[0].motor.run(levelDir);
    trainState[1].motor.run(levelDir);

}
#endif

static uint32_t initialDelay; //!< Initial setup delay in milliseconds

/*! \brief Perform one-time setup of sound and train controllers.
 */
void setup()
{
    analogReference(EXTERNAL); // use 3.3V as external AREF

#if LOG_ENABLED
    // Setup hardware serial port
    Serial.begin(CONSOLE_BAUD_RATE);
#if 0 // HardwareSerial always returns true, so this block is useless
    while (!Serial) {
        ; // wait for serial port to connect. Needed for native USB port only
    }
#endif
    LOG_COUT(info) << F("Setup entered") << LOG_ENDLINE;
#endif
#if SYNCHRONIZE_STATION_WITH_AMBIENT_TRACK1
    trainState[0].synchronizeAmbientSounds();
#endif
#if SYNCHRONIZE_STATION_WITH_AMBIENT_TRACK2
    trainState[1].synchronizeAmbientSounds();
#endif
    // configure pins to digital mode to read Hall effect sensors
    // OBSOLETE - INPUT_PULLUP is used to obtain stable readings if cable is unplugged
    for (int t = 0; t < MAX_TRAIN_TRACKS; t += 1) {
        for (int s = 0; s < MAX_SENSORS_PER_TRACK; s += 1) {
#if LOG_ENABLED
            LOG_COUT(info) << F("track ") << t << F(" subscript=") << s << F(" pin=") << (int) trackSensorPins[t][s] << LOG_ENDLINE;
#endif
            pinMode(trackSensorPins[t][s], INPUT);
        }
    }

    soundController.init();
#if LOG_ENABLED
    LOG_COUT(info) << "back from init" << LOG_ENDLINE;
    LOG_COUT(info) << F("status=") << soundController.audioPlayer.readState() << LOG_ENDLINE;
#endif

#if OLED_ENABLED
    // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
    display.begin(); // Address 0x3D for 128x64
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(20, 20);
    display.println(F("Hello"));

    // Show initial display buffer contents on the screen --
    // the library initializes this with an Adafruit splash screen.
    display.display();
#endif

    initialDelay = millis() + 1000;

    pinMode(PIN_INTERRUPT0, INPUT); // sound busy
    pinMode(PIN_INTERRUPT1, INPUT_PULLUP); // motion sensor, manual trigger
}


/*! \brief Do deferred setup.
 *
 * \param bootIntervalSequence is incremented each time the routine is
 * called allowing easy support for initialization phases.
 * \param currentMS is the value obtained from millis().
 *
 * \return time of next deferred event in relative milliseconds since boot.
 * \retval 0 indicates no more deferred events.
 */
static uint32_t setupAfterInitialDelay(unsigned char bootIntervalSequence, uint32_t currentMS)
{
    ClockWithRollover::MillisecondValue ms(currentMS);
    ClockWithRollover now(ms);

    // update analog read of cycle duration
    analogDial.updateValue(); // initial reading to begin stabilization
    for (uint8_t i = 0; i < MAX_TRAIN_TRACKS; i += 1) {
        uint8_t enabled = checkIfTrackEnabled(i);
#if LOG_ENABLED
        LOG_COUT(info) << F("track ") << i << F(" enabled=") << trackEnabled[i] << LOG_ENDLINE;
#endif
    }
#if LOG_ENABLED
    LOG_COUT(info) << F("Audio init") << LOG_ENDLINE;
#endif
    // bool ready = soundController.waitUntilAvailable(MAX_AUDIO_TIMEOUT);
#if LOG_ENABLED
    LOG_COUT(info) << F("status=") << soundController.audioPlayer.readState() << LOG_ENDLINE;

#if ENABLE_CONSOLE_COMMANDS > 1
    soundController.audioSerial.listen();
#endif
    //  LOG_COUT(info) << F("Ready ") << ready << LOG_ENDLINE;
    LOG_COUT(info) << F("status=") << soundController.audioPlayer.readState() << LOG_ENDLINE;
    LOG_COUT(info) << F("EQ=") << soundController.audioPlayer.readEQ() << LOG_ENDLINE; //read EQ setting
    LOG_COUT(info) << F("File counts=") << soundController.audioPlayer.readFileCounts(DFPLAYER_DEVICE_SD) << LOG_ENDLINE; //read all file counts in SD card
    LOG_COUT(info) << F("Current file number=") << soundController.audioPlayer.readCurrentFileNumber() << LOG_ENDLINE; //read current play file number
    LOG_COUT(info) << F("File counts in folder 1=") << soundController.audioPlayer.readFileCountsInFolder(1) << LOG_ENDLINE; //read file counts in folder SD:/01

#if ENABLE_CONSOLE_COMMANDS > 2
    LOG_COUT(error) << F("STOP LISTENING") << LOG_ENDLINE;
    soundController.audioSerial.stopListening();
#endif
    LOG_COUT(info) << F("Trigger boot sound") << LOG_ENDLINE;
#endif
    soundController.queueSoundRequest(now, 1, 98); // play boot complete sound
    uint8_t lastState = soundController.waitForSoundState(TrainSoundController::SOUND_BUSY, MAX_AUDIO_TIMEOUT);
    if (lastState == TrainSoundController::SOUND_BUSY) {
#if LOG_ENABLED
        LOG_COUT(info) << F("Wait sound complete") << LOG_ENDLINE;
#endif
        soundController.waitForSoundState(TrainSoundController::SOUND_NOT_BUSY, static_cast<uint32_t>(30) * 1000);
    }

    //  attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT0), interrupt0Routine, CHANGE);
    //  attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT1), interrupt1Routine, CHANGE);

    resetAllTracks(now);
    analogDial.updateValue(); // take another reading to aid stabilization
    return (0); // no more deferred work needed
}

/*! \brief Main activity loop
 *
 * Primary logic:
 *   - process any pending LED flashing
 *   - read the current analog value
 *   - check if sound board has changed state
 *   - check to see if doppler radar motion detector has triggere
 *   - process any train movements on each respective track
 *
 * \note Called approximately every 5 milliseconds.
 */
void loop()
{
    static ClockWithRollover now;  //!< tracks current time, static to retain state
    static unsigned char bootPhase; // use 1 byte to allow multiple initialization phases
//     static uint32_t loopInvocations; //!< Number of loop() invocations / initialDelay

    uint32_t currentTime = millis();
    ClockWithRollover::MillisecondValue ms(currentTime);
    now = ms; // rollover logic handled by assignment operator

#if ENABLE_LED_BLINK_NOTIFICATIONS
    LEDalarm.processBlinking(currentTime);
#endif
    // update analog read of cycle duration
    analogDial.updateValue();

//    loopInvocations += 1;
    if (initialDelay != 0) {
        if (initialDelay > currentTime) { // still not there...
            return;
        }
        bootPhase += 1;
        initialDelay = setupAfterInitialDelay(bootPhase, currentTime);
        if (initialDelay != 0) { // more deferred work to be done
            return;
        }
    }
#if ENABLE_CONSOLE_COMMANDS
    readAndProcessConsoleCommands(currentTime);
#endif
#if (DUMP_STATE_INTERVAL_SECONDS != 0) && LOG_ENABLED
    if (currentTime > (lastDumpInterval + (static_cast<int32_t>(1000) * DUMP_STATE_INTERVAL_SECONDS))) {
        dumpDebugState(currentTime, lastDumpInterval);
        lastDumpInterval = currentTime;
    }
#endif // end DUMP_STATE_INTERVAL_SECONDS != 0

    // handle change in sound processor state
    uint8_t lastState = digitalRead(PIN_INTERRUPT0);
    if (soundController.getLastSoundStateRead() != lastState) { // busy status changed
#if LOG_ENABLED && DEBUG_SOUND_CONTROLLER
        LOG_COUT(info) << F("lastSoundStateSet=") << soundController.getLastSoundStateSet() <<
                       F(" new=") << lastState << LOG_ENDLINE;
#endif
        soundController.setLastSoundStateRead(lastState);
        soundController.changeSoundState(now, lastState);
    } else {
        soundController.processSoundQueue(now);
    }

    // check for motion trigger
    MotionTriggerState::eButtonState buttonState = motionTrigger.pollButton(currentTime);
    switch (buttonState) {
    case MotionTriggerState::NOT_PRESSED:
    case MotionTriggerState::INITIAL_PRESS:
    case MotionTriggerState::LONG_PRESS_DETECTED:
        break;
    case MotionTriggerState::SHORT_PRESS:
        processShortDurationPress(now);
        break;
    case MotionTriggerState::LONG_PRESS:
        processLongDurationPress(now);
        break;
    }

#if TEST_MOTOR_SPEED
    changeMotor(currentTime);
    return;
#endif
    if ((endOfActiveCycle < now) && endOfActiveCycle.isClockSet()) {
#if LOG_ENABLED && DEBUG_TRACK_POWER
        LOG_COUT(warn) << F("active cycle expired") << LOG_ENDLINE;
#endif
        startOfActiveCycle.resetClock();
        endOfActiveCycle.resetClock();
        for (uint8_t track = 0; track < MAX_TRAIN_TRACKS; track += 1) {
            trainState[track].clearQueue();
            trainState[track].slowAndStop(now, SLOW_DOWN_INTERVAL_MS);
        }
        LEDalarm.setFlashes(sizeof(alarmFlashes_endOfCycle), alarmFlashes_endOfCycle);
        return;
    }

    for (uint8_t track = 0; track < MAX_TRAIN_TRACKS; track += 1) {
        monitorTrainMovement(now, track);
    } // end for each track

} // end loop()

/* vim: set filetype=cpp : */
/* vim: set expandtab shiftwidth=4 tabstop=4 : */