#!/usr/bin/perl
# =========================================================================
# FILE: DnB.pl                                                  11-05-2018
#
# SERVICES: D&B Model Railroad Control Program 
#
# DESCRIPTION:
#   This program is used to automate operations on the D&B HO scale model 
#   railroad. This Raspberry Pi based program and associated electronics 
#   replaces the Parallax Basic Stamp based control system. Refer to the
#   program help and the following for documentation related to this new 
#   control system.
#
#   Notebook: D&B Model Railroad Raspberry Pi Control
#   Webpage: http://www.buczynski.com/DnB_rr/DnB_Rpi_Overview.html
#
#   For information on the Basic Stamp version, refer to the following.
#
#   Notebook: D&B Basic Stamp
#   Webpage: http://www.buczynski.com/DnB_rr/DnB_Overview.shtml
#
#   This program is written for perl on Raspberry Pi 3.
#
# PERL VERSION: 5.24.1
#
#      (c) Copyright (c) 2018  Don Buczynski.  All Rights Reserved.
# =========================================================================
BEGIN {
   use Cwd;
   ($ExecutableName) = ($0 =~ /([^\/\\]*)$/);
   if (length($ExecutableName) == length($0)) {
      $WorkingDir = cwd();
   }
   else {
      $WorkingDir = substr($0, 0, rindex($0, "/"));
   }
   unshift (@INC, $WorkingDir);
   $libDir = join("/../", $WorkingDir, "lib");
   unshift (@INC, $libDir) if (-e $libDir);

   srand;      # Initialize random number seed.
}

# -------------------------------------------------------------------------
# External module definitions.
use Getopt::Std;
use Forks::Super;
use Forks::Super::Debug;
use DnB_Mainline;
use DnB_Sensor;
use DnB_Signal;
use DnB_Turnout;
use DnB_GradeCrossing;
use DnB_Yard;
use DnB_Message;
use POSIX qw(:signal_h :errno_h :sys_wait_h);
use RPi::WiringPi;
use RPi::Const qw(:all);

# -------------------------------------------------------------------------
# Global variables.
$DebugLevel = -1;                  # Debug level, set by -d option.

$MainRun = 0;                      # Main program flag.
                                   #    1 = Initialize complete.
                                   #    2 = Main loop active.

# The following variables hold id values for running child processes.
$SignalChildPid = 0;               # Pid of SignalChildProcess.
$ButtonChildPid = 0;               # Pid of ButtonChildProcess.     
$KeypadChildPid = 0;               # Pid of KeypadChildProcess.
$GcChildPid01 = 0;                 # Pid of GradeCrossing01 process.
$GcChildPid02 = 0;                 # Pid of GradeCrossing02 process.

$ChildName = "Main";               # Name of child process, for ctrl+c.

$SerialDev = "/dev/serial0";       # Default serial port device
$SerialBaud = 115200;              # Default baud rate;
$SerialPort = "";                  # Set if serial port is open.

$TurnoutFile = join("/", $WorkingDir, "TurnoutDataFile.txt");
$SoundPlayer = join("/", $WorkingDir, "omxplayer_quiet --vol -2000 -o both " .
                                      "$WorkingDir/mp3");

# ==============================================================================
# DnB Program Start/Stop
#
# DnB.pl is a perl program that runs under Raspbian operating system; a Debian 
# based Linux specifically developed for the Raspberry Pi. To prevent possible 
# corruption of the SD card software, the OS should be properly shutdown prior
# to removing power from the Raspberry Pi. Further, the software is designed to
# start and run "headless"; that it, without interaction with the Linux CLI for 
# normal operations. The following describes these processes.
#
# Startup:
#
# SYSTEMD is used to automatically launch DnB.pl once Linux has completed boot.
# The file /lib/systemd/system/DnB.service, with permission 644, initiates this 
# process.
#
#    [Unit]
#    Description=DnB model railroad control program.
#    After=multi-user.target
#
#    [Service]
#    Type=idle
#    ExecStart=/usr/bin/perl /home/pi/perl/DnB.pl
#
#    [Install]
#    WantedBy=multi-user.target
#
# This above is configured with the following.
#
#    sudo systemctl daemon-reload
#    sudo systemctl enable DnB.service
#
# In addition, if the shutdown button is held down, the DnB.pl program will 
# acknowledge the button hold and then exit automatic startup. The Linux OS
# will not be shutdown permitting normal Raspbian CLI/GUI interaction using
# monitor, mouse, and keyboard.
# 
# Shutdown:
#
# A momentary contact button is connected across GPIO21 and ground. GPIO21 is
# configured as an input with pullup enabled. This circuit is monitored by
# DnB.pl. Detection of a button press initiates a 10 second delay during which
# five tones will be sounded. At the end of the delay, the Raspbian OS will be
# shutdown using 'sudo shutdown -h now'. Once the Raspberry Pi green activity
# LED is no longer flashing, it is safe to power off the layout electronics.
#
# During the delay period, shutdown can be aborted by another press of the 
# shutdown button.

# ==============================================================================
# RPi Sound Player Modification
#
# A modification is needed to the RPi sound player script, /usr/bin/omxplayer.
# The omxplayer is used by DnB to play mp3 sound files which contain warning
# and keypad input confirmation tones. The changes suppress default messages 
# that the omxplayer outputs. This enables the omxplayer and DnB programs to 
# run asynchronously and non-blocking. The $SoundPlayer variable above has 
# been set to use this name and path.
#
#    1. cd to directory containing DnB.pl.
#    2. cp /usr/bin/omxplayer omxplayer_quiet
#    3. edit omxplayer_quiet
#       a. change: OMXPLAYER_DIR=`dirname $0` 
#          to:     OMXPLAYER_DIR=/usr/bin
#       b. change: LD_LIBRARY_PATH="$OMXPLAYER_LIBS${LD_LIBRARY_PATH:+
#                     :$LD_LIBRARY_PATH}" $OMXPLAYER_BIN "$@"
#          to:     LD_LIBRARY_PATH="$OMXPLAYER_LIBS${LD_LIBRARY_PATH:+
#                     :$LD_LIBRARY_PATH}" $OMXPLAYER_BIN "$@" > /dev/null 2>&1
#    4. Save file and exit editor.
#
# All sound files are output using the $SoundPlayer variable by the PlaySound
# subroutine located in DnB_Yard.

# ==============================================================================
# Turnout Related Data
#
# The DriverBoardAddress hash holds the I2C address of the servo driver boards. 
# The following hash is used only when the TurnoutDataFile file is unavailable
# for load. The mapped address is applied to the driver board referenced in
# the %TurnoutData hash below.

%ServoBoardAddress = ('1' => 0x41, '2' => 0x42);

# The TurnoutData hash stores the information used to position the turnouts on
# the layout. The storage structure is known as a 'hash-of-hashes'. This type
# of data structure simplifys access by the code. Only a pointer to the hash
# is needed when communicating the dataset to code blocks. 
#
#  %TurnoutData (
#     Turnout1 => {
#        Pid => <pid of forked MoveTurnout process>
#        Addr => <driver_board_I2C_address>,
#        Port => <driver_board_servo_port>, 
#        Pos => <last_servo_pwm_position>,
#        Rate => <servo_move_rate_pwm_per_sec>,
#        Open => <turnout_open_pwm_value>,
#        Middle => <turnout_middle_pwm_value>,
#        Close => <turnout_close_pwm_value>, 
#        MinPos => <minimum_servo_pwm_position>,
#        MaxPos => <maximum_servo_pwm_position>,
#        MinRate => <minimum_servo_slew_rate>,
#        MaxRate => <maximum_servo_slew_rate>,
#        Id => <Identification string>
#     },
#     Turnout2 => {
#        ... 
#     }
#  );
#
# The following initializes the hash with default data. Default data is used
# to write the initial TurnoutDataFile contents. Thereafter, these default
# values are overwritten during program startup by TurnoutDataFile file load. 
# This allows the user to change the operating values for Rate, Open, Close,
# and Min/Max for layout needs. A 'Rate' value of 450 moves the turnout servo
# from Open (350) to Close (850) in 1.1 seconds.
#
# Important: The ~100 hz PCA9685 refresh rate calculation in I2C_InitServoDriver
#            results in MinPos:300 and MaxPos:900 for the SG90 servo. When
#            adjusting turnout point positions, do not exceed these limits. 

%TurnoutData = (
   '01' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 0, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900, 
            'Id' => 'Mainline T01'},
   '02' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 1, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Mainline T02'},
   '03' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 2, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Mainline T03'},
   '04' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 3, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'spare'},
   '05' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 4, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Mainline T05'},
   '06' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 5, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Mainline T06'},
   '07' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 6, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Mainline T07'},
   '08' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 7, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T08'},
   '09' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 8, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T09'},
   '10' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 9, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T10'},
   '11' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 10, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T11'},
   '12' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 11, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T12'},
   '13' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 12, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T13'},
   '14' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 13, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T14'},
   '15' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 14, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T15'},
   '16' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'1'}, 'Port' => 15, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T16'},
   '17' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 0, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T17'},
   '18' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 1, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T18'},
   '19' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 2, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T19'},
   '20' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 3, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T20'},
   '21' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 4, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T21'},
   '22' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 5, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T22'},
   '23' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 6, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T23'},
   '24' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 7, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T24'},
   '25' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 8, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Yard T25'},
   '26' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 9, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Turntable T26'},
   '27' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 10, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Turntable T27'},
   '28' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 11, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'spare'},
   '29' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 12, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'spare'},
   '30' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 13, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 350, 'Middle' => 600, 
            'Close' => 850, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Semaphore'},
   '31' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 14, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 450, 'Middle' => 600, 
            'Close' => 750, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Grade Crossing 2 Gate 1'},
   '32' => {'Pid' => 0, 'Addr' => $ServoBoardAddress{'2'}, 'Port' => 15, 
            'Pos' => 600, 'Rate' => 450, 'Open' => 450, 'Middle' => 600, 
            'Close' => 750, 'MinPos' => 300, 'MaxPos' => 900,
            'Id' => 'Grade Crossing 2 Gate 2'});

# Since MoveTurnout is a slow process, each turnout position change is forked 
# to prevent blocking the main program. A simple fork does not support passing 
# of child data back to the parent. Since the final turnout position is needed 
# from the child, a 'piped fork' is used. At fork activation, the child process
# pipes STDOUT and STDERR are mapped to the TurnoutData hash, 'Pos' and 'Pid' 
# respectively, for the turnout being moved.
#
# Following fork activation, the child process pid is stored in the TurnoutData 
# hash for the turnout. The turnout move is 'inprogress' until this pid value is
# again zero. The child process prints the final turnout position to STDOUT and
# zero to STDERR. These values are written directly to the %TurnoutData hash due
# to the pipe configurations set at activation. 

# ==============================================================================
# Signal Related Data
#
# - Track Plan -
#
# When reduced to simplest form, the trackplan consists of the following elec-
# trical blocks (Bxx) and searchlight signals (Sxx). The character < or > shows 
# the train direction controlled (or lamp reflectors if you want to think of it 
# that way). 

#                         S03>     <S04             S09>     <S10
#   /==B01==\        <S02 /====B04====\        <S08 /====B07====\====\
#            =====B03=====             =====B06=====            B09  B10
#   \==B02==/ S01>        \====B05====/ S07>        \====B08====/====/
#                         S05>     <S06             S11>     <S12

# The following rules are used to illuminate the signals.
#
#   Signal        Condition
#   ------        ---------
#    Off          Unoccupied block not being approached
#   Green         Approaching unoccupied block
#    Red          Approaching occupied block
#   Yellow        Approaching unoccupied block; subsequent block occupied

# - Signal Control -
#
# The GpioData hash holds the Raspberry Pi GPIO pin data that is used to access
# the driver hardware controlling the layout signals and power polarity relays.
# The pins are manipulated by RPi::WiringPi to communicate with the 74HC595 shift 
# register which in turn drives the signal LEDs. The power polarity relays are
# driven directly by the GPIO pins. The Init_SignalDriver code creates the 
# necessary pin objects and stores the object pointer in this hash.

# GPIO set to hardware PWM mode. 

%GpioData = (
   'GPIO17_XLAT' => {'Desc' => '74HC595 Data Latch', 'Mode' => 1, 
                     'Obj' => 0},
   'GPIO23_OUTE' => {'Desc' => '74HC595 Output Enable', 'Mode' => 1,
                     'Obj' => 0},                     
   'GPIO27_SCLK' => {'Desc' => '74HC595 Serial Clock', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO22_DATA' => {'Desc' => '74HC595 Data', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO5_PR01'  => {'Desc' => 'Power Polarity relay 01', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO6_PR02'  => {'Desc' => 'Power Polarity relay 02', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO13_PR03' => {'Desc' => 'Power Polarity relay 03', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO19_FE01' => {'Desc' => 'Keypad 01 first entry', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO26_FE02' => {'Desc' => 'Keypad 02 first entry', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO20_TEST' => {'Desc' => 'Timing Test signal', 'Mode' => 1,
                     'Obj' => 0},
   'GPIO21_SHDN' => {'Desc' => 'Shutdown button', 'Mode' => 0,
                     'Obj' => 0});

# RPi::Pin needs numeric value inputs. Defenitions are as follows.
#    'Mode': 0=Input, 1=Output, 2=PWM_OUT, 3=GPIO_CLOCK

# The SignalData hash stores information about the signals. Each entry uses a
# consecutive pair of bits in the shift register; bits 0 and 1 for signal 1, 
# bits 2 and 3 for signal 2, etc. A bicolor LED is wired across the bit pair 
# and illuminates red for one voltage polarity (e.g. bit 0 high, bit 1 low) and 
# green for the opposite polarity (bit 0 low, bit 1 high). If both bits are the 
# same state, high or low, the LED is off. This provides the needed signal states; 
# off, red, and green. The specific state for each signal is determined by the 
# code using the block detector inputs.

# The color yellow is achieved by rapidly switching a signal between red 
# and green. The human eye perceives this action as the color yellow. The
# SignalChildProcess performs this by using two internal shift register
# buffers. Yellow signals are red in one and green in the other. The buffers
# are alternately sent to the shift register.

# The bits associated with SignalData 13-16 are used for the grade crossing 
# signals. See the 'Grade Crossing Data' section below for information as to 
# how these bits are utilized.

%SignalData = (
   '01' => {'Bits' => '0,1',   'Current' => 'Off', 'Desc' => 'Track B3 control'},
   '02' => {'Bits' => '2,3',   'Current' => 'Off', 'Desc' => 'Track B3 control'},
   '03' => {'Bits' => '4,5',   'Current' => 'Off', 'Desc' => 'Track B4 control'},
   '04' => {'Bits' => '6,7',   'Current' => 'Off', 'Desc' => 'Track B4 control'},
   '05' => {'Bits' => '8,9',   'Current' => 'Off', 'Desc' => 'Track B5 control'},
   '06' => {'Bits' => '10,11', 'Current' => 'Off', 'Desc' => 'Track B5 control'},
   '07' => {'Bits' => '12,13', 'Current' => 'Off', 'Desc' => 'Track B6 control'},
   '08' => {'Bits' => '14,15', 'Current' => 'Off', 'Desc' => 'Track B6 control'},
   '09' => {'Bits' => '16,17', 'Current' => 'Off', 'Desc' => 'Track B7 control'},
   '10' => {'Bits' => '18,19', 'Current' => 'Off', 'Desc' => 'Track B7 control'},
   '11' => {'Bits' => '20,11', 'Current' => 'Off', 'Desc' => 'Track B8 control'},
   '12' => {'Bits' => '22,23', 'Current' => 'Off', 'Desc' => 'Track B8 control'},
   '13' => {'Bits' => '24,25', 'Current' => 'Off', 'Desc' => 'GC 1 LEDs'},
   '14' => {'Bits' => '26,27', 'Current' => 'Off', 'Desc' => 'GC 2 LEDs'},
   '15' => {'Bits' => '28,29', 'Current' => 'Off', 'Desc' => 'GC Bell 1a & 1b'},
   '16' => {'Bits' => '30,31', 'Current' => 'Off', 'Desc' => 'GC Bell 2a & 2b'});

# The algorithm used for setting a signal's color is based upon the track plan 
# and signalling rules. Each track block, when occupied by a train, results in 
# a set of signal indications as described in the %SignalColor hash. The color
# values are derrived by assuming a single occupied track block. 

# ActiveBlock   S01  S02  S03  S04  S05  S06  S07  S08  S09  S10  S11  S12
# -----------   ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---
#    B01        GRN  YEL
#    B02        GRN  YEL
#    B03        RED  RED  GRN  YEL  GRN  YEL
#    B04        YEL  GRN  RED  RED            GRN  YEL
#    B05        YEL  GRN            RED  RED  GRN  YEL
#    B06                  YEL  GRN  YEL  GRN  RED  RED  GRN  YEL  GRN  YEL
#    B07                                      YEL  GRN  RED  RED
#    B08                                      YEL  GRN            RED  RED
#    B09                                                YEL  GRN  YEL  GRN 
#    B10                                                YEL  GRN  YEL  GRN 

# When multiple track blocks are occupied, color priority is applied since a 
# signal might have more than one color indication. For example, if only B03 
# is occupied, the color for S03 is green. If both B03 and B04 are occupied, 
# S03 could be either green or red. The correct color to display is red. When 
# a signal would display more than one color, the following color priority is
# used: Red = highest, Yellow = medium, Green = lowest.

# To accomplish this prioritization, the block sensor inputs are processed three 
# times, 1st for green indications, 2nd for yellow indications, and lastly for 
# red indications. In this way, red overwrites green or yellow, and yellow 
# overwrites green.

# Primary key maps to the %SensorBit hash. The secondary key maps to the
# %SignalData hash.

%SignalColor = (
   '01' => {'Grn' => '01',          'Yel' => '02'},
   '02' => {'Grn' => '01',          'Yel' => '02'},
   '03' => {'Grn' => '03,05',       'Yel' => '04,06',       'Red' => '01,02'},
   '04' => {'Grn' => '02,07',       'Yel' => '01,08',       'Red' => '03,04'},
   '05' => {'Grn' => '02,07',       'Yel' => '01,08',       'Red' => '05,06'},
   '06' => {'Grn' => '04,06,09,11', 'Yel' => '03,05,10,12', 'Red' => '07,08'},
   '07' => {'Grn' => '08',          'Yel' => '07',          'Red' => '09,10'},
   '08' => {'Grn' => '08',          'Yel' => '07',          'Red' => '11,12'},
   '09' => {'Grn' => '10,12',       'Yel' => '09,11'},
   '10' => {'Grn' => '10,12',       'Yel' => '09,11'});

# - Semaphore Signal - 
#
# The SemaphoreData hash holds information related to the Semaphore signal. This
# signal is modeled as the old style moveable flag board semaphore. The lamp in 
# this signal is a low voltage incandescent bulb. The lamp is driven by a bit of
# the associated signal bit pair defined in the SignalData hash. The SignalData 
# primary key (signal number) is the primary key used in the SemaphoreData hash. 

%SemaphoreData = (
   '08' => {'Servo' => '30', 'InMotion' => 0, 'Lamp' => 'Off'});

# The SemaphoreData hash identifies the TurnoutData servo used with the signal. 
# The 'Open' (green), 'Middle' (yellow) and 'Closed' (red) positions of the flag 
# board can be adjusted like the turnouts by modifying the TurnoutDataFile file.

# The SemaphoreData hash is also used to persist control data. Due to signal flag
# board motion, multiple calls to the SetSemaphoreSignal code will occur before a
# previously requested color position is completed.
  
# When setting signal colors, the main code checks the SemaphoreData hash for the 
# signal being processed. If present, the SetSemaphoreSignal routine us used for
# setting its color. See the description of this code for further information.

# ==============================================================================
# Grade Crossing Data
#
# There are two grade crossings on the DnB model railroad, each with flashing
# signals and one with crossing gates. Across-the-track infrared sensors are 
# used to detect train presence. These sensors are mapped to bit positions in 
# the %SensorBit hash. At program startup, a dedicated child process is started
# for each grade crossing. The child process is used to handle the signal lamp 
# flashing. Gate positioning, and logic to send the 'start' and 'stop' commands
# to the signal child process, is handled by the ProcessGradeCrossing code. 

%GradeCrossingData = (
   '01' => {'AprEast' => '10',     # %SensorBit east approach sensor bit.
            'Road' => '11',        # %SensorBit road sensor bit.
            'AprWest' => '12',     # %SensorBit west approach sensor bit.
            'Signal' => '13',      # %SignalData lamp bits.
            'Gate' => '',          # %TurnoutData gate servo(s).
            'State' => 'idle',     # Current grade crossing state.
            'AprTimer' => 0,       # Approach activity timer.
            'RoadTimer' => 0,      # Road activity timer.
            'DepTimer' => 0,       # Departure activity timer.
            'SigRun' => 'off',     # Signal lamps active.
            'GateDelay' => 0,      # Not used, no gates for this signal.
            'GateServo' => 0,      # Not used, no gates for this signal.
            'Sound' => '15'        # %SignalData sound control bits.
           },
   '02' => {'AprEast' => '13',     # %SensorBit east approach sensor bit.
            'Road' => '14',        # %SensorBit road sensor bit.
            'AprWest' => '15',     # %SensorBit west approach sensor bit.
            'Signal' => '14',      # %SignalData lamp bits.
            'Gate' => '31,32',     # %TurnoutData gate servo(s).
            'State' => 'idle',     # Current grade crossing state. 
            'AprTimer' => 0,       # Approach activity timer.
            'RoadTimer' => 0,      # Road activity timer.
            'DepTimer' => 0,       # Departure activity timer.
            'SigRun' => 'off',     # Signal lamps active.
            'GateDelay' => 0,      # Working delay for 'gateLower' state.
            'GateServo' => 0,      # Working servo for 'gateRaise' state.
            'Sound' => '16'        # %SignalData sound control bits.
           }); 

# The Signal number maps to the SignalData hash. The grade crossing signals are
# wired to red only Leds, one to each bit of the signal position. When the signal
# is set to 'Red', one lamp will illuminate. When set to 'Grn', the other signal 
# Led illuminates. When set to 'Off' both Leds are off. This methodology saves
# 74HC595 shift register bits and facilitates the use of common signal color code.

# The grade crossing with gates maps to the %TurnoutData hash for controlling the
# associated servos. A lowered gate is set by using the 'Close' parameter value
# in the %TurnoutData hash. A raised gate uses the 'Open' parameter value. Adjust
# these values as needed to achieve the desired motion, rate, and end positions.

# Both signals have an associated sound module which produces grade crossing bell
# sound effects. The sound effects are switched on/off by bits in the 74HC595 
# shift register. The 'Sound' parameter identifies these bits in the SignalData
# hash. Two sound effects are available and are selected by setting binary '10' 
# (bell only) and '01' (bell + train noise).   
 
# ==============================================================================
# Sensor Related Data
#
# The SensorChip hash holds the I2C addresses of the I/O PI Plus boards. The 
# mapped address is applied to the sensors referenced in the %SensorData hash 
# below. Each I/O Pi Plus board has twp MCP23017 chips. The sensor initializa-
# tion code establishes an object reference for each chip and stores it in this
# hash for later use to read sensor input.

%SensorChip = (
   '1' => {'Addr' => 0x20, 'Obj' => 0},
   '2' => {'Addr' => 0x21, 'Obj' => 0},
   '3' => {'Addr' => 0x22, 'Obj' => 0},
   '4' => {'Addr' => 0x23, 'Obj' => 0});

# The MCP23017 has a number of internal registers that are used to read the
# sensor inputs. These registers are defined as follows. See the MCP23017 
# data sheet for usage information. These register addresses are dependent on 
# IOCON.BANK being set to 0. (Established by I2C_InitSensorDriver). 

%MCP23017 = (
   'IODIRA' => 0x00, 'IODIRB' => 0x01, 'IOPOLA' => 0x02, 'IOPOLB' => 0x03,
   'IOCON'  => 0x0A, 'GPPUA' => 0x0C,  'GPPUB' => 0x0D,  'GPIOA'  => 0x12, 
   'GPIOB' =>  0x13, 'OLATA' => 0x14,  'OLATB' => 0x15);

# The SensorState hash stores the active track sensor information associated 
# with chips 1 and 2. The program periodically reads each sensor chip and 
# sets this hash accordingly.

%SensorState = ('1' => 0, '2' => 0);

# There are 16 bits per MCP23017 chip. They are defined in the SensorBit hash.

%SensorBit = (
   '00' => {'Chip' => '1', 'Bit' => 'GPIOA0', 'Desc' => 'Block detector B01'},
   '01' => {'Chip' => '1', 'Bit' => 'GPIOA1', 'Desc' => 'Block detector B02'},
   '02' => {'Chip' => '1', 'Bit' => 'GPIOA2', 'Desc' => 'Block detector B03'},
   '03' => {'Chip' => '1', 'Bit' => 'GPIOA3', 'Desc' => 'Block detector B04'},
   '04' => {'Chip' => '1', 'Bit' => 'GPIOA4', 'Desc' => 'Block detector B05'},
   '05' => {'Chip' => '1', 'Bit' => 'GPIOA5', 'Desc' => 'Block detector B06'},
   '06' => {'Chip' => '1', 'Bit' => 'GPIOA6', 'Desc' => 'Block detector B07'},
   '07' => {'Chip' => '1', 'Bit' => 'GPIOA7', 'Desc' => 'Block detector B08'},
   '08' => {'Chip' => '1', 'Bit' => 'GPIOB0', 'Desc' => 'Block detector B09'},
   '09' => {'Chip' => '1', 'Bit' => 'GPIOB1', 'Desc' => 'Block detector B10'},
   '10' => {'Chip' => '1', 'Bit' => 'GPIOB2', 'Desc' => 'GC 1 AprEast'},
   '11' => {'Chip' => '1', 'Bit' => 'GPIOB3', 'Desc' => 'GC 1 Road'},
   '12' => {'Chip' => '1', 'Bit' => 'GPIOB4', 'Desc' => 'GC 1 AprWest'},
   '13' => {'Chip' => '1', 'Bit' => 'GPIOB5', 'Desc' => 'GC 2 AprEast'},
   '14' => {'Chip' => '1', 'Bit' => 'GPIOB6', 'Desc' => 'GC 2 Road'},
   '15' => {'Chip' => '1', 'Bit' => 'GPIOB7', 'Desc' => 'GC 2 AprWest'},
   '16' => {'Chip' => '2', 'Bit' => 'GPIOA0', 'Desc' => 'Sensor S1'},
   '17' => {'Chip' => '2', 'Bit' => 'GPIOA1', 'Desc' => 'Sensor S2'},
   '18' => {'Chip' => '2', 'Bit' => 'GPIOA2', 'Desc' => 'Sensor S3'},
   '19' => {'Chip' => '2', 'Bit' => 'GPIOA3', 'Desc' => 'Sensor S4 (spare)'},
   '20' => {'Chip' => '2', 'Bit' => 'GPIOA4', 'Desc' => 'Sensor S5'},
   '21' => {'Chip' => '2', 'Bit' => 'GPIOA5', 'Desc' => 'Sensor S6'},
   '22' => {'Chip' => '2', 'Bit' => 'GPIOA6', 'Desc' => 'Sensor S7'},
   '23' => {'Chip' => '2', 'Bit' => 'GPIOA7', 'Desc' => 'Sensor S8'},
   '24' => {'Chip' => '2', 'Bit' => 'GPIOB0', 'Desc' => 'Sensor S9'},
   '25' => {'Chip' => '2', 'Bit' => 'GPIOB1', 'Desc' => 'Unused'},
   '26' => {'Chip' => '2', 'Bit' => 'GPIOB2', 'Desc' => 'Unused'},
   '27' => {'Chip' => '2', 'Bit' => 'GPIOB3', 'Desc' => 'Unused'},
   '28' => {'Chip' => '2', 'Bit' => 'GPIOB4', 'Desc' => 'Unused'},
   '29' => {'Chip' => '2', 'Bit' => 'GPIOB5', 'Desc' => 'Unused'},
   '30' => {'Chip' => '2', 'Bit' => 'GPIOB6', 'Desc' => 'Unused'},
   '31' => {'Chip' => '2', 'Bit' => 'GPIOB7', 'Desc' => 'Unused'});

# Track Plan: Reverse Loop and Hold-over Tracks

# The trackage involved with this section is hidden and used for train trip 
# hold-over and return. Two sidings are available each with a train presence 
# block detector (Bx), track power polarity reverse relay (Px), and optical 
# sensors (Sx) to detect train movement. Three turnouts (Tx) are used to move
# trains in and out of this section.

#           ----------------- B1/P1 -----------------
#          /                                         \
#         /  ---------------- B2/P2 ----------------  \
#        /  /                                       \  \
#        \ | S2                                      | / S3
#         \|                                         |/
#       T2 \                                         / T3
#           -------------------    ------------------
#                              \  /
#                            T1 |/
#                               | S1
#                               |
#                               B3
#                               |
#                               ~

# Reverse loop operation requires that for an inbound or outbound operation, 
# with respect to a siding, the rail polarity must match mainline rail polarity.
# This rail polarity match is required only while power drawing portions of a 
# train are in transit across the siding rail gaps.

# In operation, a train on the mainline approaches the reverse loop. It is 
# detected by sensor S1. If block detector B1 is inactive, T1, T2, and P1 are 
# set to direct the train to siding B1. If block detector B1 is active, T1, T3, 
# and P2 are set to direct the train to siding B2. If B2 is also active, the 
# train wreck warning is sounded. Turnouts are used this way to take advantage 
# of the 'straight' side of hidden turnouts T2 and T3 to minimize derailments. 
# Trains always move clockwise through siding B1 and counter-clockwise through 
# siding B2.

# A train leaving B1 or B2 will be detected by S3 or S2 respectively. T1/T3/P1 
# or T1/T2/P2 are set to direct the train back onto the mainline.

# For an inbound or outbound operation, it is necessary to disable acting on S1 
# active indications following the initial one. For the inbound direction, this 
# prevents turnouts from changing as the block detector begins reporting the 
# presence of a train. In the outbound direction, it prevents assumption of an 
# inbound train and T1 operation.

# Stopping or backing a inbound or outbound train will have no effect on these
# operations unless the outbound sensor S2 or S3 has been reached. If so, the 
# turnouts and block power polarity will be set for an outbound condition and 
# incorrectly set for a backup operation. A train should not be backed up once 
# it is more than half way into a siding.

# Track Plan: Midway Sidings

#                                              S5   T5
#          ------------------ B4 ----------------------- B3 -- ~
#         /                                        /
#        /   ---------------- B5 ------------------
#       /   /
#       \  | S6                 
#        \ | 
#         \| T6
#          | 
#          B6
#          |
#          ~

# The track involved with this section provides a place for mainline trains to 
# pass each other. The associated turnouts simulate proto typical turnouts that 
# are "spring loaded" to a specific position. When entering, the train is always 
# directed to a specific track. When exiting, the turnout points are positioned
# to permit train passage. Once the last car of the train passes through the 
# turnout, its points are set back to the "normal" position.

# Normal position routes a train approching T5 from B3 to siding B5. A train
# approaching T6 from B6 is routed to siding B4. A train leaving B4 or B5 will
# be detected by sensors S5 or S6 respectively. The points of T5 or T6 will be 
# set to direct the train back onto the mainline. A retriggerable timeout is 
# used to debounce the S5 and S6 sensor inputs. Three seconds after the last car 
# transits the sensor, the turnout is repositioned to "normal".

# Track Plan: Yard Approach Wye

#            -------- B10 -------
#           /                    \
#          /--------- B9 ---------\
#         |                        |
#         B7                      B8
#          \                      /
#           \         P3         / 
#            \                  /
#         S8  \                /  S9
#              \-----   ------/
#                T7  \ / 
#                     |
#                     B6
#                     |  S7
#                     ~

# The track involved with this section provides a "wye" turnout; the legs of 
# which are approach tracks leading to opposite ends of the yard. This forms a 
# reverse loop that includes B7 through B10 and all of the yard tracks. The
# blocks are individual only for the purpose of signaling. Tracks leading to 
# and including the yard tracks from T7 are wired to polarity control relay P3.

# Turnout T7 is only partially controlled. The last set route will be used for 
# trains in B6 approaching T7 unless manually changed by the train engineer. The
# T7 turnout points will be set automatically for B7 or B8 trains approaching T7
# when detected by S8 or S9. The power polarity relay P3 will be set based on the
# position of T7 to yard track power polarity matches B6 track power.

# In all cases, it is not necessary to "ignore" sensor inputs in either direction 
# of travel. Detections by S8 or S9 following S7 will not change T7 or P3 from 
# their current states. The same is true for S7 detections following S8 or S9.

# The TrackData hash, primary key sensor number, stores information that is used 
# to set turnouts and track power polarity based on train movement that activates
# sensor (Sx) and block (Bx) input.
 
%TrackData = (
   '01' => {'Timeout' => 0, 'Last' => 'B2', 'Direction' => 'In', 
            'WaitB3Inact' => 0},
   '02' => {'Timeout' => 0},
   '03' => {'Timeout' => 0}, 
   '04' => {0},
   '05' => {'Timeout' => 0, 'Inactive' => 'Open', 'Active' => 'Close',
            'ManualSet' => 0, 'Locked' => 0}, 
   '06' => {'Timeout' => 0, 'Inactive' => 'Close', 'Active' => 'Open',
            'ManualSet' => 0, 'Locked' => 0}, 
   '07' => {'Timeout' => 0, 'Polarity' => 0}, 
   '08' => {'Timeout' => 0},
   '09' => {'Timeout' => 0});

# ==============================================================================
# Keypad User Input
#
# The KeypadData hash holds information related to push button keypad input.
# A 'Storm K Range' 4x4 button keypad matrix is connected to a MCP23017 port. 
# Within the keypad, normally open push buttons are connected to the inter-
# section of each row and column. Pressing a button will cause the associated 
# row and column to be electrically connected. By driving the columns and 
# scanning the rows, the pressed button can be determined.

#     row/col   1   2   3   4
#               |   |   |   |
#       A ------0---1---2---3--
#               |   |   |   |
#       B ------4---5---6---7--
#               |   |   |   |
#       C ------8---9---A---B--
#               |   |   |   |
#       D ------C---D---E---F--
#               |   |   |   |
 
# See DnB_Sensor::ReadKeypad subroutine for keypad to MCP23017 pin mapping.    
 
%KeypadData = (
   '01' => {'Chip' => '3', 'Row' => 'GPIOA', 'Col' => 'OLATA', 'Last' => -1,
            'PressTime' => 0, 'Entry1' => -1, 'Gpio' => 'GPIO19_FE01'});

   # Note: Chip 3 hardcoded and initialized for Storm keypad in
   # DnB_Sensor::I2C_InitSensorDriver

# The first pressed button number will be stored in 'Entry1'. Two button presses 
# are needed to set a yard route. 'Gpio' identifies the GPIO used for the keypad 
# first entry indicator. If a second button is not entered within 2 seconds, the
# first key press is discarded.

# Non-matrix buttons are identified in the %ButtonData hash. These are single
# bit sized values corresponding to a button press. A 'Storm K Range' 1x4 button 
# keypad is connected to a MCP23017 port.

#     button    D   C   B   A
#               |   |   |   |
#               o---o---o---o-- common

# See DnB_Sensor::GetButton subroutine for keypad to MCP23017 pin mapping.    

%ButtonData = (
   '00' => {'Chip' => '4', 'Bit' => 'GPIOA0', 'Last' => 0, 
            'Desc' => 'Turnout T5 toggle', 'PressTime' => 0, 'Turnout1' => '05', 
            'Turnout2' => '06'},
   '01' => {'Chip' => '4', 'Bit' => 'GPIOA1', 'Last' => 0, 
            'Desc' => 'Turnout T6 toggle', 'PressTime' => 0, 'Turnout1' => '06',
            'Turnout2' => '05'},
   '02' => {'Chip' => '4', 'Bit' => 'GPIOA2', 'Last' => 0, 
            'Desc' => 'Turnout T7 open', 'Turnout' => '07'},
   '03' => {'Chip' => '4', 'Bit' => 'GPIOA3', 'Last' => 0, 
            'Desc' => 'Turnout T7 close', 'Turnout' => '07'},
   'FF' => {'Gpio' => 'GPIO21_SHDN', 'Wait' => 0, 'Shutdown' => 0, 'Step' => 0,
            'Time' => 0, 'Tones' => 'G,F,E,D,C,C_'});

# The T5 and T6 toggle buttons provide for manually toggling the position of 
# the respective turnout. This functionality is used for special train operations 
# involving this section of track. Button input is ignored if the respective 
# turnout is performing an inprogress timing operation. After manually toggling 
# T5 or T6 to the "non-normal" position, these turnouts will automatically reset 
# to their normal position once the train completes its transit of the turnout. 

# Turnouts T5 or T6 can be "locked" into the non-normal position by pressing the 
# appropriate turnout toggle button a second time within .5 second of the first 
# depression. The turnout will remain in the non-normal position until manually 
# set to the normal position using the respective toggle button.
# 
# Both T5 and T6 cannot be locked at the same time; a derailment would occur. 
# Locking either T5 or T6 permits a train to be stopped on one of the sidings 
# for an extended period of time and not interfere with mainline traffic 
# movements using the other track. To unlock a turnout, double press the turnout
# toggle button.

# Buttons are provided for manually toggling the position of turnout T7. This 
# functionality is used for selecting the desired approach track to the yard. 
# Button input is ignored if the Wye retriggerable timeout counter is non-zero 
# indicating that a train is transitting the turnout. Manual change will be 
# ignored until one second after the last active detection by S7, S8, or S9.

# ==============================================================================
# Yard Route Data
#
# The YardRouteData hash holds information used to set the turnouts (Tx) of the 
# yard and approach tracks. The following diagrams illustrates the track and 
# turnouts involved.
#                             ~
#                             | T7
#                             |\
#     ------------------------  -------------------------------------
#    /                                                               \
#   /                                                                 \
#  1              \                   /     /   /   /    /   /   /     2
# /                \                 /     /   /   /    /   /   /       \
# \                 \               /     /   /   /    /   /   /        /
#  \                 14            15    6   7   8    9   10  11       /
#   \                 \           /     /   /   /    /   /   /        /
#    \   ------- 13 ---\T25   T22/--T23/   /   /    /   /   /        /
#     \                 \       /     /   /   /    /   /   /        /
#      \    ---- 12 --T24\-----/     /T18/T19/    /T20/T21/        /
#       \                           /            /           T10  /
#     T8 \--------T12\----------T16/--- 5 ---T17/----/T15------/-/ T11
#         \   T9      16                            16        / /
#          \-\---------\T13------------ 4 ------T14/---------/ /
#             \                                               /
#              \----------------------- 3 -------------------/
#
# Yard and approach tracks are assigned a number; 1 through 16. The track
# number corresponds to the numbered keypad buttons. A route is specified 
# by keying in two track numbers. The first number entered is the "from" 
# track. It is the track currently occupied by the train. The second number 
# entered is the "to" track. It is the desired destination track for the 
# train. Once both numbers are input, the turnouts for the specified route 
# will be set appropriately. Key combinations that do not correspond to a 
# valid route will be ignored.
#
# Note: The keypad returns 0-F and these numbers are also used in the
# %YardRouteData index keys. These hexadecimal numbers correspond to tracks 
# 1-16. 
#
# Keying in the same number for the "from" and "to" tracks will set the
# turnouts to route just the specified track. This is useful for the
# following operations.
#
# Track 4 or 5: Will set all turnouts on both of these tracks to their
#               normal (straight) position.
# Track 16:     Will open the four turnouts T12 through T15 for a "run
#               around" operation. Consecutive track 16 entry will close
#               all four turnouts.
#
# There are two special cases that must be handled. For a track 4 to 5 or
# track 5 to 4 request, the turnouts used are train direction dependent.
# Since the train direction is not known, the code initially sets turnouts
# T12 and T13 for track 4 to 5 and turnouts T14 and T15 for track 5 to 4. 
# If the same track command is consecutively entered, the code inverts the 
# settings of turnouts T12/T13 and T14/T15.
#
# Only the turnouts for the selected route will be affected, all other 
# turnouts retain their current position. Turnout positions are stored as 
# they are set during operations. This information is referenced during 
# subsequent operations to skip the setting of turnouts already in the 
# proper position.
#
# To facilitate keypad entry, an indicator is positioned on the keypad. 
# This indicator will be illuminate when the first track number is entered. 
# It will extinguished when the second track number is entered.
#
# The %YardRouteData primary index is made up of a 'R' and two hexadecimal
# characters. The first character is the "from" track number. The second 
# character is the "to" track number. The value for each index is a comma 
# separated list of turnout numbers and their required position.

%YardRouteData = (
   'Control' => {'Inprogress' => 0, 'Timeout' => 0, 'Route' => "", 'Step' => 0},
   'R02' => 'T08:Close,T09:Open',
   'R03' => 'T08:Close,T09:Close,T13:Close,T14:Close',
   'R04' => 'T08:Open,T12:Close,T16:Close,T17:Close,T15:Close',
   'R05' => 'T08:Open,T12:Close,T16:Open,T18:Open,T23:Close',
   'R06' => 'T08:Open,T12:Close,T16:Open,T18:Close,T19:Open',
   'R07' => 'T08:Open,T12:Close,T16:Open,T18:Close,T19:Close',
   'R08' => 'T08:Open,T12:Close,T16:Close,T17:Open,T20:Open,T21:Open',
   'R09' => 'T08:Open,T12:Close,T16:Close,T17:Open,T20:Open,T21:Close',
   'R0A' => 'T08:Open,T12:Close,T16:Close,T17:Open,T20:Close',
   'R12' => 'T11:Close',
   'R13' => 'T11:Open,T10:Close,T14:Close,T13:Close',
   'R14' => 'T11:Open,T10:Open,T15:Close,T17:Close,T16:Close,T12:Close',
   'R20' => 'T09:Open,T08:Close',
   'R21' => 'T11:Close',
   'R22' => 'T08:Close,T09:Open,T11:Close',
   'R30' => 'T14:Close,T13:Close,T09:Close,T08:Close',
   'R31' => 'T10:Close,T11:Open,T14:Close,T13:Close',
   'R33' => 'T08:Close,T09:Close,T13:Close,T12:Close,T14:Close,' .
            'T15:Close,T10:Close,T11:Open',
   'R34' => 'T13:Open,T12:Open,T14:Close,T15:Close',
   'r34' => 'T13:Close,T12:Close,T14:Open,T15:Open',
   'R3E' => 'T13:Close,T12:Close,T14:Open,T15:Open',   # See SetRoute routine.
   'R3F' => 'T14:Close,T15:Close,T13:Open,T12:Open',   # See SetRoute routine.
   'R40' => 'T15:Close,T17:Close,T16:Close,T12:Close,T08:Open',
   'R41' => 'T15:Close,T17:Close,T16:Close,T12:Close,T10:Open,T11:Open',
   'R43' => 'T13:Close,T12:Close,T14:Open,T15:Open',
   'r43' => 'T13:Open,T12:Open,T14:Close,T15:Close',
   'R44' => 'T12:Close,T13:Close,T16:Close,T17:Close,T15:Close,T14:Close',
   'R45' => 'T16:Open,T19:Open,T18:Open,T23:Close',
   'R46' => 'T16:Open,T19:Open,T18:Close',
   'R47' => 'T16:Open,T19:Close',
   'R48' => 'T12:Close,T16:Close,T17:Open,T20:Open,T21:Open',
   'R49' => 'T12:Close,T16:Close,T17:Open,T20:Open,T21:Close',
   'R4A' => 'T12:Close,T16:Close,T17:Open,T20:Close',
   'R50' => 'T23:Close,T18:Open,T16:Open,T12:Close,T08:Open',
   'R54' => 'T23:Close,T18:Open,T19:Open,T16:Open,T12:Close',
   'R55' => 'T23:Close,T18:Open,T19:Open',
   'R5B' => 'T23:Open,T22:Close,T24:Close',
   'R5C' => 'T23:Open,T22:Close,T24:Open,T25:Close',
   'R5D' => 'T23:Open,T22:Close,T24:Open,T25:Open',
   'R60' => 'T19:Open,T18:Close,T16:Open,T12:Close,T08:Open',
   'R64' => 'T23:Close,T18:Open,T19:Open,T16:Open,T12:Close',
   'R66' => 'T19:Open,T18:Close',
   'R70' => 'T19:Close,T18:Close,T16:Open,T12:Close,T08:Open',
   'R74' => 'T19:Close,T18:Close,T16:Open,T12:Close',
   'R77' => 'T19:Close,T18:Close',
   'R80' => 'T21:Open,T20:Open,T17:Open,T16:Close,T12:Close,T08:Open',
   'R84' => 'T21:Open,T20:Open,T17:Open,T16:Close,T12:Close',
   'R88' => 'T21:Open,T20:Open',
   'R90' => 'T21:Close,T20:Open,T17:Open,T16:Close,T12:Close,T08:Open',
   'R94' => 'T21:Close,T20:Open,T17:Open,T16:Close,T12:Close',
   'R99' => 'T21:Close,T20:Open',
   'RA0' => 'T20:Close,T17:Open,T16:Close,T12:Close,T08:Open',
   'RA4' => 'T20:Close,T17:Open,T16:Close,T12:Close',
   'RAA' => 'T20:Close',
   'RB5' => 'T24:Close,T22:Close,T23:Open',
   'RBB' => 'T24:Close,T22:Close',
   'RBE' => 'T24:Close,T22:Open',
   'RC5' => 'T25:Close,T24:Open,T22:Close,T23:Open',
   'RCC' => 'T25:Close,T24:Open,T22:Close',
   'RCE' => 'T25:Close,T24:Open,T22:Open',
   'RD5' => 'T25:Open,T24:Open,T22:Close,T23:Open',
   'RDD' => 'T25:Open,T24:Open,T22:Close',
   'RDE' => 'T25:Open,T24:Open,T22:Open',
   'REB' => 'T22:Open,T24:Close',
   'REC' => 'T22:Open,T24:Open,T25:Close',
   'RED' => 'T22:Open,T24:Open,T25:Open',
   'REE' => 'T22:Open',
   'RFF' => 'T12:Open,T13:Open,T14:Open,T15:Open',
   'rFF' => 'T12:Close,T13:Close,T14:Close,T15:Close');

# ==============================================================================
# Child Process Priority
#
# A number of the processing functions are performed as child processes to the 
# main code. Child process priority (fork os_priority) is used to balance overall
# program flow. SignalChildProcess is timing sensitive due to the toggling of 
# red/green to produce a yellow signal indication. Turnout open/close operations
# may cause main code blocking until an inprogress operation completes. 
#
# Normal linux priority for a program is 0. Priority above normal is set with a
# positive value. Priority below normal is set with a negative value.
#
# Process                Priority
# ------------------     --------
# SignalChildProcess        6
# ButtonChildProcess        4
# KeypadChildProcess        4
# MoveTurnout               3
# GcChildProcess            1
# Main code                 0

# ==============================================================================

$UsageText = (qq(
===== Help for $ExecutableName =================================================
This program is used to automate operations on the D&B HO scale model railroad. 
This Raspberry Pi based program and associated electronics replaces the Parallax
Basic Stamp based control system. Refer to the following for details.

Notebook: D&B Model Railroad, Raspberry Pi Control
Webpage: <tbd>

For information on the Basic Stamp version, refer to the following.

Notebook: D&B Basic Stamp
Webpage: http://www.buczynski.com/DnB_rr/DnB_Overview.shtml

This program is coded in perl and runs under the Raspbian OS. The RPI::WiringPi 
perl module, written by Steve Bertrand, interfaces the various Raspberry Pi 
hardware functions, e.g. serial communication and GPIO, with perl.

The shutdown button must be used to properly shutdown the Raspbian OS prior to 
removing power from the layout electronics. This is important to prevent possible
corruption of the SD card software. It is safe to power off the electronics once
the green activity LED on the end of the lower board, the Raspberry Pi, does not
flash for about 5 seconds. The DnB program can be safely terminated using ctrl+c
when manually started from the command line.

The DnB.pl program is configured to start automatically as part of Raspbian OS 
boot. Hold down the shutdown button prior to, and during power-on to cause the 
DnB program to terminate without OS shutdown.  

The Raspberry Pi serial port can be used to communicate messages to a monitor 
terminal. A USB->COM device such as the Adafruit P954 cable, which also performs
level shifting, is used to connect the Pi to an external computer running a 
terminal emulator program. e.g. PuTTy or terraterm. GPIO pin connections on the 
Pi end are: 6 (Gnd, blk), 8 (Txd, wht), 10 (Rxd, grn). Set terminal emulator to 
115200,8,N,1 for the COM port being used on the USB end.
 
This control system uses SG90 hobby servos to better modeling proto-typical 
turnout movement. Two Adafruit I2C 16-Channel servo boards are used. The indivi-
dual servo positions are controlled by the pulse width values set in these driver 
boards by the DnB program. Last position information for each turnout is saved as
part of normal shutdown. It is used for servo positioning on the subsequent power
up or program restart. The crossing gate and semaphore servos are also controlled
through by these driver boards.

The file holding the servo position information, TurnoutDataFile.txt, can be user
modified using a text editor. Typically, the 'Open', 'Close', and 'Rate' values 
are adjusted for the desired turnout operation. The changed values will be used 
on the subsequent program start. Should the file become hopelessly corrupt, it 
can be restored to defaults using the -f option. A backup of the existing file 
will be made.   

The trackside signals are controlled using 74HC595 shift registers. Since each 
signal lamp utilizes a single red/green LED, internally wired back-to-back, two 
shift register bits are needed for each lamp to obtain the desired four state 
indications; off, red, green, and yellow. This is similar to the previous Basic 
Stamp design. The grade crossing signal lamps are also controlled by this shift
register.

The block detector, sensor, and keypad inputs are interfaced using I2C 32 Channel
Pi expansion boards. These boards use the Microchip MCP23017. The keypads are 
used for turnout positioning input.

The RaspBerry Pi serial interface can be used for user interaction during 
operation. Console messages are displayed to the user. Though the DnB.pl program
operates autonomously, the following operational commands are available.

<tbd>
  
There is copious documentation contained in the program code which
explains operation in greater detail. All programs can be viewed in 
a text editor or the program listing binder.  

USAGE:
   $ExecutableName [-h] [-q] [-f] [-i] [-d <lvl>] [-c] [-m <num>] [-s [r]<range>] 
          [-t [r]<range>] [-b <range>] [-g 1|2] [-k 1|2] [-p] [-x] [-z]
              
   -h           Show program help.

   -q           Runs the program in quiet mode. Suppresses all console 
                messages. Useful when running the program using autostart. 

   -d <lvl>     Run at specified debug level; 0-3. Higher level increases 
                message verbosity. Uncomment Forks::Super::DEBUG statement 
                in main code to see Forks related debug output. Note that
                level 3 causes output of child process messages. This may
                result in a flood of message output until DnB.pl is ctrl+c
                terminated and then restarted with a lower debug level.

   -i           Detect and display the I2C addresses; runs i2cdetect in
                the background. Expected active addresses are:

                   Block detectors: 0x20
                   Track sensors:   0x21
                   Yard keypad:     0x22
                   Button input:    0x23
                   Turnouts 1-16:   0x41
                   Turnouts 17-32:  0x42
                   Not Used:        0x70 

   -c           Send console output to the serial port device.
                Device: $SerialDev   Baud: $SerialBaud

   -f           Backup existing TurnoutDataFile.txt file, if any, and
                create a new file with default values. The program exits 
                once the file is created.

   -x           Disable shutdown button check during power on. Used for
                testing when button hardware is not physically connected. 

   -z           Enable toggle of GPIO20_TEST pin. Used to view main loop 
                timing on a scope. Each code section toggles the GPIO 
                state.
                   A - Top of loop          G - Process Signals
                   B - Read sensors         H - Process Yard Route
                   C - Process holdover     I - Read keypad
                   D - Process midway       J - MidwayTrack
                   E - Process wye          K - WyeTrack 
                   F - Grade Crossing (2)   L - Shutdown button

   -m <num>     Set specified turnout to its middle position. Used for
                servo mechanical adjustment. Program exits once position
                is set. All turnouts are set if specified <num> is 0.             

   -s <range>   Run signal test. <range> specifies the signal numbers to
                use, 1 thru 12. e.g. 1 (signal 1), 1,5 (signals 1 and 5), 
                1:5 (signals 1 thru 5). Preface with 'r' (r1:5) to test 
                the specified signals in random instead of sequential
                order. <range> specified as Red, Grn, Yel, or Off will
                set all signals to the specified condition. Preface with
                'g' to include grade crossings 1 and 2. This test runs 
                until terminated by ctrl+c.

   -t <range>   Run turnout test. <range> specifies the turnout numbers
                to use, 1 thru 32. e.g. 1 (turnout 1), 1,5 (turnouts 1 
                and 5), 1:5 (turnouts 1 thru 5). Preface with 'r' (r1:5) 
                to test the specified turnouts randomly instead of sequen- 
                tial order. Add 'w' (w1:5, wr1:5) to wait for the opera-
                tion to complete before staring another. This test runs 
                until terminated by ctrl+c.

   -b <range>   Run sensor test. <range> specifies the chip numbers to
                use, 1 thru 4. e.g. 1 (chip 1), 1,2 (chips 1 and 2),
                1:4 (chips 1 thru 4). The associated sensor bits are read
                and displayed. This test runs until terminated by ctrl+c.

   -g 1|2       Run grade crossing test using the specified crossing,
                1, 2, or both (comma separated). The grade crossing lamps 
                are flashed and gates raised and lowered. This test runs 
                until terminated by ctrl+c.

   -k           Run the keypad test; pressed buttons will be displayed. 
                The 1st entry LED will toggle for each 4x4 keypad button
                press. Single/double button presses on the 1x4 keypad
                will also be displayed. This test runs until it is 
                terminated by ctrl+c.

   -p           Run the sound player test. Used to select and audition
                the available sound files. This test runs until it is 
                terminated.

==============================================================================                    

));

# =========================================================================
# MAIN PROGRAM
# =========================================================================

# Process user specified CLI options.
getopts("hqcipfxzkb:d:t:s:m:g:");

$DebugLevel = $opt_d if (defined($opt_d));
# $Forks::Super::DEBUG = 1;    # Uncomment to see Forks::Super debug output.

# -------------------------------------------------------------------------
# Display help text if requested.

if (defined($opt_h)) {
   print $UsageText;
   exit(0);
}

# -------------------------------------------------------------------------
# Display I2C addresses if requested.

if (defined($opt_i)) {
   print "\nActive I2C addresses:\n\n";
   system("sudo i2cdetect -y 1");
   print "\n";
   exit(0);
}   

# -------------------------------------------------------------------------
# Create new TurnoutDataFile if requested.

if (defined($opt_f)) {
   if (-e $TurnoutFile) {
      $backupFile = $TurnoutFile;
      $backupFile =~ s/txt$/bak/;
      exit(1) if (&ReadFile($TurnoutFile, \@Array, "NoTrim")); 
      exit(1) if (&WriteFile($backupFile, \@Array, ""));
      unless (-e $backupFile) {
         $result = &DisplayError("main, Failed to create backup file " .
                                 "$backupFile");
         exit(1);
      }
   }
   if (&ProcessTurnoutFile($TurnoutFile, "Write", \%TurnoutData)) {
      $result = &DisplayError("main, Failed to create $TurnoutFile");
      exit(1);
   }
   if (-e $TurnoutFile) {
      $result = &DisplayMessage("Default TurnoutDataFile successfully " .
                                "created.");
   } 
   exit(0);
}   

# -------------------------------------------------------------------------
# Setup for processing keyboard entered signals.                              

foreach $sig ('INT','QUIT','TERM') {     # Catch termination signals
   $SIG{$sig} = \&Ctrl_C;
}

# -------------------------------------------------------------------------
# Configure for buffer autoflush.
select (STDERR);
$| = 1;
select (STDOUT);
$| = 1;                              

# -------------------------------------------------------------------------
# Kill orphan child processes and parent/child intercommunication files, 
# if any. This will occur if the program abnormally terminates.

@list = `ps -ef | grep DnB.pl`;
foreach $line (@list) {
   if ($line =~ m/^\w+\s+(\d+)\s+1\s/) {
      system("kill -9 $1");
   }
}
$result = `rm -rf /dev/shm/.fh*`;

# -------------------------------------------------------------------------
# Open the serial port if specified.

if (defined($opt_c)) {
   if (&OpenSerialPort(\$SerialPort, $SerialDev, $SerialBaud)) {
      $result = &DisplayWarning("main, Failed to open serial port. $SerialDev");
   }
   unless (defined($opt_q)) {
      print STDOUT "$$ Serial port $SerialDev open, $SerialBaud baud.\n";
   }
}

# =========================================================================
# Tell the world we're up and running.

$result = &DisplayMessage("=== DnB program start ===");
$MainRun = 1;

# -------------------------------------------------------------------------
# Initialize the GPIO pins associated with the Signal LED Driver. Check the
# shutdown button. If pressed, terminate this program but don't shutdown 
# Linux OS.

if (&Init_SignalDriver(\%GpioData, scalar(keys %SignalData)*2)) {
   exit(1);
}
else {
   # Check for user press of shutdonw button to abort startup. Skip check if 
   # -x option or any test option is specified.
   unless (defined($opt_x) or defined($opt_p) or defined($opt_k) or
           defined($opt_g) or defined($opt_b) or defined($opt_t) or
           defined($opt_s) or defined($opt_m)) {       
      $buttonPress = $GpioData{'GPIO21_SHDN'}{'Obj'}->read;
      if ($buttonPress == 1) {
         print "$$ main, Shutdown button pressed. Aborting DnB startup.\n";
         print "$$ main, Specify -x option to bypass this check.\n\n";
         exit(0);
      }
   }
}

# -------------------------------------------------------------------------
# Initialize the I2C MCP23017 sensor chips on the I/O PI Plus board.

for ($chip = 1; $chip <= scalar keys(%SensorChip); $chip++) {
   if ($SensorChip{$chip} == 0) {
      $result = &DisplayDebug(1, "main, Skip chip $chip I2C_Address 0, " .
                              "code debug.");
      next;
   }
   $result = &DisplayMessage("Initializing sensor I2C MCP23017 $chip ...");
   exit(1) if (&I2C_InitSensorDriver($chip, \%MCP23017, \%SensorChip));
}

# -------------------------------------------------------------------------
# Start the 4x4 keypad child process. The stderr handle is used to send key
# press data from child to parent. Parent must periodically key data using:
# $key = Forks::Super::read_stderr($KeypadChildPid);

$kPid = fork {os_priority => 4, sub => \&KeypadChildProcess,
              child_fh => "err socket", 
              args => [ '01', \%KeypadData, \%MCP23017, \%SensorChip ] };

if (!defined($kPid)) {
   $result = &DisplayError("main, Failed to start KeypadChildProcess. $!");
   exit(1);
}
else {
   $KeypadChildPid = $kPid;
}

# -------------------------------------------------------------------------
# Start the 1x4 button keypad child process. The stderr handle is used to
# send button press data from child to parent. Parent must periodically
# use: $button = Forks::Super::read_stderr($ButtonChildPid);

$bPid = fork {os_priority => 4, sub => \&ButtonChildProcess,
              child_fh => "err socket", 
              args => [ \%ButtonData, \%MCP23017, \%SensorChip ] };

if (!defined($bPid)) {
   $result = &DisplayError("main, Failed to start ButtonChildProcess. $!");
   exit(1);
}
else {
   $ButtonChildPid = $bPid;
}

# -------------------------------------------------------------------------
# Start the signal child process.

$SignalChildPid = fork { os_priority => 6, sub => \&SignalChildProcess, 
                         child_fh => "in socket", args => [ \%GpioData ] };
$result = &DisplayDebug(1, "main, SignalChildPid: $SignalChildPid");
exit(1) if ($SignalChildPid == 0);

# -------------------------------------------------------------------------
# Start a grade crossing child process for each grade crossing.

foreach $gc (sort keys (%GradeCrossingData)) {
   $GradeCrossingData{$gc}{'Pid'} = fork { os_priority => 1, 
                                           sub => \&GcChildProcess, 
                                           child_fh => "in socket", 
                                           args => [ $gc, $SignalChildPid, 
                                                     \%GradeCrossingData, 
                                                     \%TurnoutData ] };
   $result = &DisplayDebug(1, "main, GcChildPid${gc}: " .
                              "$GradeCrossingData{$gc}{'Pid'}");
   exit(1) if ($GradeCrossingData{$gc}{'Pid'} == 0);

   $GcChildPid01 = $GradeCrossingData{$gc}{'Pid'} if ($gc eq '01');
   $GcChildPid02 = $GradeCrossingData{$gc}{'Pid'} if ($gc eq '02');
}

# -------------------------------------------------------------------------
# Load the turnout last position file. Initialize all turnout servos to their 
# center position if -m option is specified. Otherwise, set each turnout to 
# the position recorded in the file.

$result = &DisplayMessage("Reading turnout last position file ...");
$result = &ProcessTurnoutFile($TurnoutFile, "Read", \%TurnoutData);

# -------------------------------------------------------------------------
# Initialize the I2C servo driver boards to the PWM position specified in
# %TurnoutData for each servo. Exit if positioning servo(s) for mechanical 
# adjustment of turnout points (-m option).

if (defined($opt_m)) {
   $result = &InitTurnouts(\%ServoBoardAddress, \%TurnoutData, $opt_m);
   exit($result);
}
else {
   $result = &InitTurnouts(\%ServoBoardAddress, \%TurnoutData, '');
   exit($result) if ($result);
}

# =========================================================================
# Perfom CLI specified testing.

# -----
# Run TestSensors in DnB_Sensor.pm if specified.
# -----
if (defined($opt_b)) {
   exit(&TestSensors($opt_b, \%MCP23017, \%SensorChip, \%SensorState));
}

# -----
# Run TestKeypad in DnB_Sensor.pm if specified.
# -----
if (defined($opt_k)) {
   exit(&TestKeypad('1', \%KeypadData, \%ButtonData, \%GpioData, \%MCP23017, 
                    \%SensorChip, $KeypadChildPid, $ButtonChildPid));
}

# -----
# Run TestGradeCrossing in DnB_GradeCrossing.pm if specified.
# -----
if (defined($opt_g)) {
   select (undef, undef, undef, .5);   # Delay for GcChildProcess message output.
   exit(&TestGradeCrossing($opt_g, \%GradeCrossingData, \%TurnoutData));
}

# -----
# Run TestSignals in DnB_Signal.pm if specified. Options for signal testing can 
# include grade crossing and gate (turnout code) testing.
# -----
if (defined($opt_s)) {
   select (undef, undef, undef, .5);   # Delay for SignalChildProcess message.
   exit(&TestSignals($opt_s, $SignalChildPid, \%SignalData, \%GradeCrossingData, 
                     \%SemaphoreData, \%TurnoutData));
}

# -----
# Run TestTurnouts in DnB_Turnout.pm if specified.
# -----
exit(&TestTurnouts($opt_t, \%TurnoutData)) if (defined($opt_t));

# -----
# Run TestSound in DnB_Yard.pm if specified.
# -----
if (defined($opt_p)) {
   $SoundFileDir = substr($SoundPlayer, rindex($SoundPlayer, " ")+1);
   exit(&TestSound($SoundFileDir));
}

# =========================================================================
# Start main program loop.

$result = &DisplayMessage("=== DnB main loop start ===");
$MainRun = 2;

while ($MainRun) {

# -----
# Read the sensors and store values in %SensorState hash.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # A
   $result = &DisplayDebug(2, "main - Driver: $$SensorChip{'1'}{'Obj'}");

   $SensorState{'1'} = 
      ($SensorChip{'1'}{'Obj'}->read_byte($MCP23017{'GPIOB'}) << 8) |
       $SensorChip{'1'}{'Obj'}->read_byte($MCP23017{'GPIOA'});
   $SensorState{'2'} = 
      ($SensorChip{'2'}{'Obj'}->read_byte($MCP23017{'GPIOB'}) << 8) |
       $SensorChip{'2'}{'Obj'}->read_byte($MCP23017{'GPIOA'});

# -----
# Set the sensor activated turnouts and polarity relays.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # B
   &ProcessHoldover(\%TrackData, \%SensorBit, \%SensorState, \%TurnoutData,
                    \%GpioData);
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # C
   &ProcessMidway(\%TrackData, \%SensorBit, \%SensorState, \%TurnoutData);
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # D
   &ProcessWye(\%TrackData, \%SensorBit, \%SensorState, \%TurnoutData, 
                    \%GpioData);

# -----
# Call ProcessGradeCrossing to check and process the grade crossing sensors.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # E
   foreach $gc (sort keys(%GradeCrossingData)) {
      $result = &ProcessGradeCrossing($gc, \%GradeCrossingData, \%SensorBit, 
                \%TurnoutData, \%MCP23017, \%SensorState);
      # last; # uncomment for one signal debug
   }

# -----
# Set track signals using the block detector sensor bits.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # F
   %signalWork = ();                      # Initialize working hash.
   foreach $color ('Grn','Yel','Red') {
      foreach $block ('00','01','02','03','04','05','06','07','08','09') {
         $sensorBits = $SensorState{ $SensorBit{$block}{'Chip'} };
         $bitMask = 1;
         if ($SensorBit{$block}{'Bit'} =~ m/(GPIO.)(\d)/) {
            $bitMask = $bitMask << 8 if ($1 eq 'GPIOB');
            $bitMask = $bitMask << $2;
            $result = &DisplayDebug(2, "main, color: $color   block: $block" .
                          "   sensorBits: " . sprintf("%0.16b", $sensorBits) .
                          "   bitMask: " . sprintf("%0.16b", $bitMask));
         }
         if ($sensorBits & $bitMask) {   # Block active if 1

            # Available color settings?
            if (exists $SignalColor{$block}{$color}) {
               @sigColorList = split(",", $SignalColor{$block}{$color});
               $result = &DisplayDebug(2, "main, color: $color   " .
                                          "sigColorList: @sigColorList");
               foreach $signal (@sigColorList) {
                  $signalWork{$signal} = $color;
               }
            }
         }
      }
   }

   # Activate the new signal values.
   for $signal (1..12) {
      $signal = "0$signal" if (length($signal) == 1);
      if (exists ($signalWork{$signal})) {
         $color = $signalWork{$signal};
      }
      else {
         $color = 'Off';
      }

      # Skip if signal is already at the proper color.
      next if ($SignalData{$signal}{Current} eq $color);

      # Set new signal color.
      if (exists ($SemaphoreData{$signal})) {
         if (&SetSemaphoreSignal($signal, $color, $SignalChildPid, 
                          \%SignalData, \%SemaphoreData, \%TurnoutData)) {
            $result = &DisplayError("main, SetSemaphoreSignal $signal " .
                                    "'$color' returned error.");
         }
      }
      else {
         if (&SetSignalColor($signal, $color, $SignalChildPid, 
                             \%SignalData, '')) {
            $result = &DisplayError("main, SetSignalColor $signal " .
                                    "'$color' returned error.");
         }
      }
   }

# -----
# Process inprogress turnout route setting.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # G
   &YardRoute(\%YardRouteData, \%TurnoutData);

# -----
# Get and process yard route input from user.
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # H
   &GetYardRoute(\%YardRouteData, \%KeypadData, \%GpioData, $KeypadChildPid);

# -----
# Process user single button input.
# -----
   $buttonInput = Forks::Super::read_stderr($ButtonChildPid);
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # I
   &MidwayTrack($buttonInput, \%ButtonData, \%TurnoutData, \%TrackData);
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # J
   &WyeTrack($buttonInput, \%ButtonData, \%TurnoutData, \%TrackData);

# -----
# Initiate shutdown if requested by the user. ShutdownRequest will return 1
# if the shutdown button has been pressed and not aborted with another press
# within 5 seconds. 
# -----
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(1) if (defined($opt_z));  # K
   if (&ShutdownRequest('FF', \%ButtonData, \%GpioData)) {

      # Save current turnout data to file.
      $result = &ProcessTurnoutFile($TurnoutFile, "Write", \%TurnoutData);
      $result = &DisplayMessage("=== DnB program termination ===\n");
      $MainRun = 0;
      system("sudo shutdown -h now");
   }
   $GpioData{'GPIO20_TEST'}{'Obj'}->write(0) if (defined($opt_z));  # L
   select (undef, undef, undef, .1);   # Loop delay
}

exit(0);
