# ============================================================================
# FILE: DnB_Mainline.pm                                             6/22/2019
#
# SERVICES:  DnB TRACK PROCESSING FUNCTIONS
#
# DESCRIPTION:
#    This perl module provides mainline track processing related functions used 
#    by the DnB model railroad control program. 
#
# PERL VERSION: 5.24.1
#
# =============================================================================

# -----------------------------------------------------------------------------
# Package Declaration
# -----------------------------------------------------------------------------

package DnB_Mainline;
require Exporter;
@ISA = qw(Exporter);

@EXPORT = qw(
   ProcessHoldover
   ProcessMidway
   ProcessWye
   HoldoverTrack
   MidwayTrack
   WyeTrack
   CheckTurnout
);

use DnB_Message;
use DnB_Sensor;
use DnB_Turnout;

# =============================================================================
# FUNCTION:  ProcessHoldover
#
# DESCRIPTION:
#    This routine performs the operational functions related to the holdover
#    track section. Functions include turnout point positioning and setting 
#    of track power polarity.
#
#    Retriggerable timers in %TrackData are used for S1, S2, and S3 to ensure
#    that a route is set only once for as long as the train activates the 
#    sensor.
#
#    The S1, S2, and S3 sensors also retrigger the 'RouteTime' if a temporary 
#    holdover route has be set ('RouteLocked') via button input. When a route
#    has been set, no other ProcessHoldover functions are performed.  
#
# CALLING SYNTAX:
#    $result = &ProcessHoldover(\%TrackData, \%SensorBit, \%SensorState, 
#                               \%TurnoutData, \%GpioData);
#
# ARGUMENTS:
#    $TrackData          Pointer to %TrackData hash. 
#    $SensorBit          Pointer to %SensorBit hash.
#    $SensorState        Pointer to %SensorState hash. 
#    $TurnoutData        Pointer to %TurnoutData hash.
#    $GpioData           Pointer to %GpioData hash. (polarity relays) 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub ProcessHoldover {
   my($TrackData, $SensorBit, $SensorState, $TurnoutData, $GpioData) = @_;
   my($result, $moveResult, $device, $turnout, $position, $gpio, $value, $check);
   my(%bitPos) = ('B1' => '00', 'B2' => '01', 'B3' => '02', 'S1' => '16',
                  'S2' => '17', 'S3' => '18');
   my(%routes) = (
      'InB1'  => 'T01:Close,T02:Close,T03:Close,GPIO6_PR02:0',
      'InB2'  => 'T01:Open,T03:Close,T02:Close,GPIO5_PR01:0',
      'OutB1' => 'T03:Open,T01:Open,GPIO6_PR02:1',
      'OutB2' => 'T02:Open,T01:Close,GPIO5_PR01:1');

   my(@route) = ();
   my($cTime) = time;

   $result = &DisplayDebug(2, "ProcessHoldover entry ...");

# --- RouteLocked related processing. -----------------------------------------
   if ($$TrackData{'01'}{'RouteLocked'} == 1) {
      if (&GetSensorBit($bitPos{'S1'}, $SensorBit, $SensorState) == 1 or
          &GetSensorBit($bitPos{'S2'}, $SensorBit, $SensorState) == 1 or
          &GetSensorBit($bitPos{'S3'}, $SensorBit, $SensorState) == 1) {
         $$TrackData{'01'}{'ManualRouteTime'} = time + 30;
      }
      return 0;
   }

# --- Processing for S1 sensor disable. ---------------------------------------
# S1 sensor input is ignored for an outbound train to prevent improper turnout
# positioning; 'Direction' is set to 'Out'. 'Direction' is set back to 'In' and
# outbound route flags are reset when track block B3 is no longer occupied. 
# Holdover turnouts are then returned to default positions.
 
   if ($$TrackData{'01'}{'Direction'} eq 'Out' and
       $$TrackData{'01'}{'WaitB3Inact'} == 1) {
      if (&GetSensorBit($bitPos{'B3'}, $SensorBit, $SensorState) == 0) {
         $result = &DisplayMessage("ProcessHoldover, block B3 is unoccupied.");
         $$TrackData{'01'}{'Direction'} = 'In';
         $$TrackData{'01'}{'WaitB3Inact'} = 0;
         @route = split(",", $routes{'InB1'});
      }
   }

# --- Sensor S1 processing. ---------------------------------------------------
   if (&GetSensorBit($bitPos{'S1'}, $SensorBit, $SensorState) == 1) {
      $result = &DisplayDebug(1, "ProcessHoldover, S1 is active.");
      if ($$TrackData{'01'}{'Direction'} eq 'Out' and
          $$TrackData{'01'}{'WaitB3Inact'} == 0) {
         $$TrackData{'01'}{'WaitB3Inact'} = 1;
         $result = &DisplayMessage("ProcessHoldover, waiting for block B3 to " .
                                   "be unoccupied.");
      }

      if ($$TrackData{'01'}{'Direction'} eq 'In' and
          &GetSensorBit($bitPos{'S2'}, $SensorBit, $SensorState) == 0 and
          &GetSensorBit($bitPos{'S3'}, $SensorBit, $SensorState) == 0) {
         if ($$TrackData{'01'}{'Timeout'} < $cTime) {  # If route not already set.

            # Alternate holdover tracks if both are unoccupied.
            if (&GetSensorBit($bitPos{'B1'}, $SensorBit, $SensorState) == 0 and
                &GetSensorBit($bitPos{'B2'}, $SensorBit, $SensorState) == 0) {
               if ($$TrackData{'01'}{'Last'} eq 'B1') {
                  $result = &DisplayMessage("ProcessHoldover, routing inbound " .
                                            "train to B2.");
                  $$TrackData{'01'}{'Last'} = 'B2';
                  @route = split(",", $routes{'InB2'});
               }
               else {
                  $result = &DisplayMessage("ProcessHoldover, routing inbound " .
                                             "train to B1.");
                  $$TrackData{'01'}{'Last'} = 'B1';
                  @route = split(",", $routes{'InB1'});
               }
            }
            else {
               if (&GetSensorBit($bitPos{'B1'}, $SensorBit, $SensorState) == 1) {
                  $result = &DisplayMessage("ProcessHoldover, B1 is active.");
                  if (&GetSensorBit($bitPos{'B2'},$SensorBit,$SensorState) == 1) {
                     $result = &DisplayMessage("ProcessHoldover, B2 is active.");
                     $result = &DisplayMessage("ProcessHoldover, inbound train " .
                                               "wreck!!!");
                     $result = &PlaySound("TrainWreck3.wav");
                  }
                  else {
                     $result = &DisplayMessage("ProcessHoldover, routing " .
                                               "inbound train to B2.");
                     @route = split(",", $routes{'InB2'});
                  }
               }
               else {
                  $result = &DisplayMessage("ProcessHoldover, routing " .
                                            "inbound train to B1.");
                  @route = split(",", $routes{'InB1'});
               }
            }
         }
         $$TrackData{'01'}{'Timeout'} = $cTime + 2;  # Disable S1 processing.
      }
   }

# --- Sensor S2 processing. ---------------------------------------------------
   elsif (&GetSensorBit($bitPos{'S2'}, $SensorBit, $SensorState) == 1) {
      $result = &DisplayDebug(1, "ProcessHoldover, S2 is active.");
      if ($$TrackData{'02'}{'Timeout'} < $cTime) {  # If route not already set.
         $result = &DisplayMessage("ProcessHoldover, routing outbound B2 " . 
                                   "train to B3.");
         @route = split(",", $routes{'OutB2'});
         $$TrackData{'01'}{'Direction'} = 'Out';
      }
      $$TrackData{'02'}{'Timeout'} = $cTime + 2;   # Disable S2 processing.
   }

# --- Sensor S3 processing. ---------------------------------------------------
   elsif (&GetSensorBit($bitPos{'S3'}, $SensorBit, $SensorState) == 1) {
      $result = &DisplayDebug(1, "ProcessHoldover, S3 is active.");
      if ($$TrackData{'03'}{'Timeout'} < $cTime) {  # If route not already set.
         $result = &DisplayMessage("ProcessHoldover, routing outbound B1 " . 
                                   "train to B3.");
         @route = split(",", $routes{'OutB1'});
         $$TrackData{'01'}{'Direction'} = 'Out';
      }
      $$TrackData{'03'}{'Timeout'} = $cTime + 2;   # Disable S3 processing.
   }

# --- Set turnouts and relays if @route is specified. --------------------------
   if ($#route >= 0) {
      foreach $device (@route) {
         if ($device =~ m/^T(\d+):(.+)/) {
            $turnout = $1;
            $position = $2;
            $moveResult = &MoveTurnout($position, $turnout, $TurnoutData);
            if ($moveResult == 1) {
               $result = &DisplayError("ProcessHoldover, Failed to set turnout " .
                                       "$turnout to $position");
            }
            else {
               $result = &DisplayMessage("ProcessHoldover, turnout $turnout " .
                                         "set to $position.");
            }
         }
         elsif ($device =~ m/^(GPIO.+?):(\d)/) {
            $gpio = $1;
            $value = $2;
            $$GpioData{$gpio}{'Obj'}->write($value);  # Set power polarity relay.
            $check = $$GpioData{$gpio}{'Obj'}->read;  # Readback and check.
            if ($check != $value) {
               $result = &DisplayError("ProcessHoldover, Failed to set power " .
                                       "relay $gpio to $value");
            }
            else {
               $result = &DisplayMessage("ProcessHoldover, relay $gpio " .
                                         "set to $value.");
            }
         }
         else {
            $result = &DisplayError("ProcessHoldover, Invalid S1 route entry: " .
                                    "$device");
            return 1;
         }
      }
   }
   return 0;
}

# =============================================================================
# FUNCTION:  ProcessMidway
#
# DESCRIPTION:
#    This routine performs the operational functions related to the midway
#    track section. Functions include turnout point positioning. A turnout
#    is not processed if previously locked by user button input.
#
#    Retriggerable timers in %TrackData are used for S5 and S6 to ensure
#    that a route is set only once for as long as the train activates the 
#    sensor.
#
#    The respective turnout it set back to the Inactive position after its
#    timer expires. This action is inhibited by a manually set position. In 
#    this case, reposition will occur after a 2nd timeout cycle.
#
# CALLING SYNTAX:
#    $result = &ProcessMidway(\%TrackData, \%SensorBit, \%SensorState, 
#                             \%TurnoutData);
#
# ARGUMENTS:
#    $TrackData          Pointer to %TrackData hash. 
#    $SensorBit          Pointer to %SensorBit hash.
#    $SensorState        Pointer to %SensorState hash. 
#    $TurnoutData        Pointer to %TurnoutData hash. 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub ProcessMidway {
   my($TrackData, $SensorBit, $SensorState, $TurnoutData) = @_;
   my($result, $moveResult, $turnout, $position);
   my(%bitPos) = ('S5' => '20', 'S6' => '21');
   my($cTime) = time;

   $result = &DisplayDebug(2, "ProcessMidway entry ...");

# --- Sensor S5 processing. ---------------------------------------------------
   if ($$TrackData{'05'}{'Locked'} == 0) {
      if (&GetSensorBit($bitPos{'S5'}, $SensorBit, $SensorState) == 1) {
         $result = &DisplayDebug(1, "ProcessMidway, S5 is active.");

         # Move turnout if no inprogress timeout. Otherwise, restart timeout.
         if ($$TurnoutData{'05'}{'Pos'} ne 
             $$TurnoutData{'05'}{ $$TrackData{'05'}{'Active'} } and
             $$TrackData{'05'}{'Timeout'} < $cTime) {

            $moveResult = &MoveTurnout($$TrackData{'05'}{'Active'}, '05', 
                                   $TurnoutData);
            if ($moveResult == 1) {
               $result = &DisplayError("ProcessMidway, Failed to set turnout " .
                                       "05 to $$TrackData{'05'}{'Active'}.");
            }
            else {
               $result = &DisplayMessage("ProcessMidway, turnout 05 set to " .
                                "active position $$TrackData{'05'}{'Active'}.");
            }
         }
         $$TrackData{'05'}{'Timeout'} = $cTime + 8;   # Retrigger timeout.
         $$TrackData{'05'}{'ManualSet'} = 0;
      }
      else {

         # Reset turnout if a timeout has completed and turnout is not in the 
         # Inactive position. Check for turnout Pid 0 prevents additional turnout 
         # setting during the move period. 
         if ($cTime >= $$TrackData{'05'}{'Timeout'} and 
             $$TrackData{'05'}{'ManualSet'} == 0 and
             $$TurnoutData{'05'}{'Pid'} == 0 and
             $$TurnoutData{'05'}{'Pos'} ne 
             $$TurnoutData{'05'}{ $$TrackData{'05'}{'Inactive'} }) {
            $result = &MoveTurnout($$TrackData{'05'}{'Inactive'}, '05', 
                                   $TurnoutData);
            if ($result == 1) {
               $result = &DisplayError("ProcessMidway, Failed to set turnout " .
                                       "05 to $$TrackData{'05'}{'Inactive'}.");
            }
            else {
               $result = &DisplayMessage("ProcessMidway, turnout 05 set to " .
                            "inactive position $$TrackData{'05'}{'Inactive'}.");
            }
         }
      }
   }

# --- Sensor S6 processing. ---------------------------------------------------
   if ($$TrackData{'06'}{'Locked'} == 0) {
      if (&GetSensorBit($bitPos{'S6'}, $SensorBit, $SensorState) == 1) {
         $result = &DisplayDebug(1, "ProcessMidway, S6 is active.");

         # Move turnout if no inprogress timeout. Otherwise, restart timeout.
         if ($$TurnoutData{'06'}{'Pos'} ne 
             $$TurnoutData{'06'}{ $$TrackData{'06'}{'Active'} } and
             $$TrackData{'06'}{'Timeout'} < $cTime) {

            $moveResult = &MoveTurnout($$TrackData{'06'}{'Active'}, '06', 
                                   $TurnoutData);
            if ($moveResult == 1) {
               $result = &DisplayError("ProcessMidway, Failed to set turnout " .
                                       "06 to $$TrackData{'06'}{'Active'}.");
            }
            else {
               $result = &DisplayMessage("ProcessMidway, turnout 06 set to " .
                                "active position $$TrackData{'06'}{'Active'}.");
            }
         }
         $$TrackData{'06'}{'Timeout'} = $cTime + 8;   # Retrigger timeout.
         $$TrackData{'06'}{'ManualSet'} = 0;
      }
      else {

         # Reset turnout if a timeout has completed and turnout is not in the 
         # Inactive position. Check for turnout Pid 0 prevents additional turnout 
         # setting during the move period. 
         if ($cTime >= $$TrackData{'06'}{'Timeout'} and 
             $$TrackData{'06'}{'ManualSet'} == 0 and
             $$TurnoutData{'06'}{'Pid'} == 0 and
             $$TurnoutData{'06'}{'Pos'} ne 
             $$TurnoutData{'06'}{ $$TrackData{'06'}{'Inactive'} }) {
            $result = &MoveTurnout($$TrackData{'06'}{'Inactive'}, '06', 
                                   $TurnoutData);
            if ($result == 1) {
               $result = &DisplayError("ProcessMidway, Failed to set turnout " .
                                       "06 to $$TrackData{'06'}{'Inactive'}.");
            }
            else {
               $result = &DisplayMessage("ProcessMidway, turnout 06 set to " .
                             "inactive position $$TrackData{'06'}{'Inactive'}.");
            }
         }
      }
   }
   return 0;
}

# =============================================================================
# FUNCTION:  ProcessWye
#
# DESCRIPTION:
#    This routine performs the operational functions related to the wye track
#    section. Functions include turnout point positioning and setting of track 
#    power polarity.
#
# CALLING SYNTAX:
#    $result = &ProcessWye(\%TrackData, \%SensorBit, \%SensorState,
#                          \%TurnoutData, \%GpioData);
#
# ARGUMENTS:
#    $TrackData          Pointer to %TrackData hash. 
#    $SensorBit          Pointer to %SensorBit hash.
#    $SensorState        Pointer to %SensorState hash. 
#    $TurnoutData        Pointer to %TurnoutData hash. 
#    $GpioData           Pointer to %GpioData hash. (polarity relays) 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub ProcessWye {
   my($TrackData, $SensorBit, $SensorState, $TurnoutData, $GpioData) = @_;
   my($result, $moveResult, $device, $turnout, $position, $gpio, $value, $check);
   my(%bitPos) = ('S7' => '22', 'S8' => '23', 'S9' => '24');
   my($cTime) = time;

   $result = &DisplayDebug(2, "ProcessWye entry ...");

# --- Sensor S7 processing. ---------------------------------------------------
   if (&GetSensorBit($bitPos{'S7'}, $SensorBit, $SensorState) == 1) {
      if ($$TrackData{'07'}{'Timeout'} < $cTime) {
         $result = &DisplayDebug(1, "ProcessWye, S7 is active.");

         # Only need to set polarity relay.
         if ($$TurnoutData{'07'}{'Pos'} eq $$TurnoutData{'07'}{'Close'}) {
            if ($$TrackData{'07'}{'Polarity'} != 0) {
               $$GpioData{GPIO13_PR03}{'Obj'}->write(0); # Set relay control bit.
               if ($$GpioData{GPIO13_PR03}{'Obj'}->read != 0) {  # Readback check.
                  $result = &DisplayError("ProcessWye S7, Failed to set power " .
                                          "relay GPIO13_PR03 to 0");
               }
               else {
                  $result = &DisplayMessage("ProcessWye S7, power relay " .
                                            "GPIO13_PR03 set to 0.");
               }
               $$TrackData{'07'}{'Polarity'} = 0;
            }
         }
         else {
            if ($$TrackData{'07'}{'Polarity'} != 1) {
               $$GpioData{GPIO13_PR03}{'Obj'}->write(1); # Set relay control bit.
               if ($$GpioData{GPIO13_PR03}{'Obj'}->read != 1) {  # Readback check.
                  $result = &DisplayError("ProcessWye S7, Failed to set power " .
                                          "relay GPIO13_PR03 to 1");
               }
               else {
                  $result = &DisplayMessage("ProcessWye S7, power relay " .
                                            "GPIO13_PR03 set to 1.");
               }
               $$TrackData{'07'}{'Polarity'} = 1;
            }   
         }
      }
      $$TrackData{'07'}{'Timeout'} = $cTime + 2;
   }

# --- Sensor S8 processing. ---------------------------------------------------
   if (&GetSensorBit($bitPos{'S8'}, $SensorBit, $SensorState) == 1) {
      if ($$TrackData{'08'}{'Timeout'} < $cTime) {
         $result = &DisplayDebug(1, "ProcessWye, S8 is active.");
         $moveResult = &MoveTurnout('Close', '07', $TurnoutData);
         if ($moveResult == 1) {
            $result = &DisplayError("ProcessWye S8, Failed to set turnout 07 " .
                                    "to Close.");
         }
         else {
            $result = &DisplayMessage("ProcessWye S8, turnout 07 set to Close.");
         }
         if ($$TrackData{'07'}{'Polarity'} != 0) {
            $$GpioData{GPIO13_PR03}{'Obj'}->write(0);  # Set relay control bit.
            if ($$GpioData{GPIO13_PR03}{'Obj'}->read != 0) {   # Readback check.
               $result = &DisplayError("ProcessWye S8, Failed to set power " .
                                       "relay GPIO13_PR03 to 0");
            }
            else {
               $result = &DisplayMessage("ProcessWye S8, power relay " .
                                         "GPIO13_PR03 set to 0.");
            }
            $$TrackData{'07'}{'Polarity'} = 0;
         }
      }
      $$TrackData{'08'}{'Timeout'} = $cTime + 2;
   }

# --- Sensor S9 processing. ---------------------------------------------------
   if (&GetSensorBit($bitPos{'S9'}, $SensorBit, $SensorState) == 1) {
      if ($$TrackData{'09'}{'Timeout'} < $cTime) {
         $result = &DisplayDebug(1, "ProcessWye, S9 is active.");
         $moveResult = &MoveTurnout('Open', '07', $TurnoutData);
         if ($moveResult == 1) {
            $result = &DisplayError("ProcessWye S9, Failed to set turnout 07 " .
                                    "to Open.");
         }
         else {
            $result = &DisplayMessage("ProcessWye S9, turnout 07 set to Open.");
         }
         if ($$TrackData{'07'}{'Polarity'} != 1) {
            $$GpioData{GPIO13_PR03}{'Obj'}->write(1);  # Set relay control bit.
            if ($$GpioData{GPIO13_PR03}{'Obj'}->read != 1) {   # Readback check.
               $result = &DisplayError("ProcessWye S9, Failed to set power " .
                                       "relay GPIO13_PR03 to 1");
            }
            else {
               $result = &DisplayMessage("ProcessWye S9, power relay " .
                                         "GPIO13_PR03 set to 1.");
            }
            $$TrackData{'07'}{'Polarity'} = 1;
         }
      }
      $$TrackData{'09'}{'Timeout'} = $cTime + 2;
   }
   return 0;
}

# =============================================================================
# FUNCTION:  HoldoverTrack
#
# DESCRIPTION:
#    This routine processes the user buttons associated with turnouts T01, T02,
#    and T03 in the Holdover track section. Four buttons are provided for user
#    input of a desired route. In response, this routine sets the turnouts as
#    needed. The turnouts will be 'locked' in the requested route and a LED
#    indicator on the keypad will be illuminated. This route will be persisted
#    until one of the following conditions occur.
#
#    1. Any button on the holdover route keypad is pressed.
#    2. No S1, S2, or S3 sensor activity for 30 seconds.
#
# CALLING SYNTAX:
#    $result = &HoldoverTrack($ButtonInput, \%TurnoutData, \%TrackData, 
#                             \%GpioData);
#
# ARGUMENTS:
#    $ButtonInput        User entered button input, if any.
#    $TurnoutData        Pointer to %TurnoutData hash.
#    $TrackData          Pointer to %TrackData hash.
#    $GpioData           Pointer to %GpioData hash. (polarity relays) 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub HoldoverTrack {
   my($ButtonInput, $TurnoutData, $TrackData, $GpioData) = @_;
   my($result, $button, $route, $gpio, $value, $device, $turnout, $position);
   my($check, $moveResult);
   my(%routes) = (
      '04' => 'T01:Close,T02:Close,GPIO6_PR02:0',
      '05' => 'T01:Close,T02:Open,GPIO5_PR01:1',
      '06' => 'T01:Open,T03:Close,GPIO5_PR01:0',
      '07' => 'T01:Open,T03:Open,GPIO6_PR02:1');
   my(@route) = ();
   $result = &DisplayDebug(2, "HoldoverTrack entry ...  ButtonInput: " .
                              "'$ButtonInput'");

   # Process new button press.
   if ($ButtonInput =~ m/s(04)/ or $ButtonInput =~ m/s(05)/ or
       $ButtonInput =~ m/s(06)/ or $ButtonInput =~ m/s(07)/) {
      $button = $1;
      $route = join('', 'R', ($button - 3));
      $result = &DisplayMessage("HoldoverTrack, route $route requested.");

      # ----------------------------------
      # If a route is currently active, reset and done.
      if ($$TrackData{'01'}{'RouteLocked'} == 1) {
         $result = &PlaySound("Unlock.wav");
         $$GpioData{'GPIO26_HLCK'}{'Obj'}->write(0);        # Button LED off
         $$TrackData{'01'}{'RouteLocked'} = 0;
         $result = &DisplayMessage("HoldoverTrack, route unlocked by button.");
         return 0;
      }
      @route = split(",", $routes{$button});

      # ----------------------------------
      # Set turnouts.
      if ($#route >= 0) { 
         foreach $device (@route) {
            if ($device =~ m/^T(\d+):(.+)/) {
               $turnout = $1;
               $position = $2;
               $moveResult = &MoveTurnout($position, $turnout, $TurnoutData);
               if ($moveResult == 1) {
                  $result = &DisplayError("ProcessHoldover, Failed to set " .
                                       "turnout $turnout to $position");
               }
               else {
                  $result = &DisplayMessage("ProcessHoldover, turnout " .
                                         "$turnout set to $position.");
               }
            }
            elsif ($device =~ m/^(GPIO.+?):(\d)/) {
               $gpio = $1;
               $value = $2;
               $$GpioData{$gpio}{'Obj'}->write($value);  # Set polarity relay.
               $check = $$GpioData{$gpio}{'Obj'}->read;  # Readback and check.
               if ($check != $value) {
                  $result = &DisplayError("HoldoverTrack, Failed to set " .
                                          "power relay $gpio to $value");
               }
               else {
                  $result = &DisplayMessage("ProcessHoldover, relay $gpio " .
                                            "set to $value.");
               }
            }
         }

         $$GpioData{'GPIO26_HLCK'}{'Obj'}->write(1);     # Button LED on
         $$TrackData{'01'}{'RouteLocked'} = 1;
         $$TrackData{'01'}{'RouteTime'} = time + 30;
         $result = &DisplayMessage("HoldoverTrack, route $route is locked.");
         $result = &PlaySound("Lock.wav");
      }
      else {
         $result = &DisplayMessage("HoldoverTrack, $route is invalid for " .
                                   "train movement direction.");
         $result = &PlaySound("GE.wav");
      }
   }

   # If a route is set, and has timed out, reset the lock.
   else {
      if ($$TrackData{'01'}{'RouteLocked'} == 1 and
          $$TrackData{'01'}{'RouteTime'} < time) {
         $result = &PlaySound("Unlock.wav");
         $$TrackData{'01'}{'RouteLocked'} = 0;
         $$GpioData{'GPIO26_HLCK'}{'Obj'}->write(0);        # Button LED off
         $result = &DisplayMessage("HoldoverTrack, route unlocked by timeout.");
      }
   }
   return 0;
}

# =============================================================================
# FUNCTION:  MidwayTrack
#
# DESCRIPTION:
#    This routine processes the user buttons associated with turnouts T05 and
#    T06. These buttons, 00 and 01, are used to manually position the turnout 
#    or lock it in its current position.
#
# CALLING SYNTAX:
#    $result = &MidwayTrack($ButtonInput, \%ButtonData, \%TurnoutData, 
#                           \%TrackData);
#
# ARGUMENTS:
#    $ButtonInput        User entered button input, if any.
#    $ButtonData         Pointer to %ButtonData hash.
#    $TurnoutData        Pointer to %TurnoutData hash.
#    $TrackData          Pointer to %TrackData hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub MidwayTrack {
   my($ButtonInput, $ButtonData, $TurnoutData, $TrackData) = @_;
   my($result, $moveResult, $turnout1, $turnout2, $position);

   $result = &DisplayDebug(2, "MidwayTrack entry ...  ButtonInput: " .
                              "'$ButtonInput'");

   # ----------------------------------
   # Process double press button input.
   if ($ButtonInput =~ m/d(00)/ or $ButtonInput =~ m/d(01)/) {
      $turnout1 = $$ButtonData{$1}{'Turnout1'};
      $turnout2 = $$ButtonData{$1}{'Turnout2'};
      $result = &DisplayDebug(1, "MidwayTrack, turnout1: $turnout1   turnout2: ".
                                 "$turnout2");

      # Ignore the button if a $turnout1 timeout or move is inprogress.
      return 0 if (&CheckTurnout($turnout1, 'MidwayTrack', $TurnoutData, 
                                 $TrackData));

      # Unlock and reposition the opposite turnout if currently locked.
      if ($$TrackData{$turnout2}{'Locked'} == 1) {

         # Ignore the button if a $turnout2 timeout or move is inprogress.
         return 0 if (&CheckTurnout($turnout2, 'MidwayTrack', $TurnoutData, 
                                    $TrackData));

         # Move $turnout2 only if necessary.
         if ($$TurnoutData{$turnout2}{'Pos'} ne 
            $$TurnoutData{$turnout2}{ $$TrackData{$turnout2}{'Inactive'} }) {

            $moveResult = &MoveTurnout($$TrackData{$turnout2}{'Inactive'}, 
                                       $turnout2, $TurnoutData);
            if ($moveResult == 1) {
               $result = &DisplayError("MidwayTrack, Failed to set " .
                                       "turnout $turnout2 to " .
                                       "$$TrackData{$turnout2}{'Inactive'}.");
               $result = &PlaySound("GE.wav");
               return 0;
            }
         }
         $$TrackData{$turnout2}{'Locked'} = 0;
         $$TrackData{$turnout2}{'ManualSet'} = 0;
         $result = &DisplayMessage("MidwayTrack, $turnout2 is unlocked.");
      }

      # Lock $turnout1.
      $$TrackData{$turnout1}{'Locked'} = 1;
      $result = &DisplayMessage("MidwayTrack, $turnout1 is locked.");
      $result = &PlaySound("Lock.wav");
   }

   # ----------------------------------
   # Process single press button input.
   elsif ($ButtonInput =~ m/s(00)/ or $ButtonInput =~ m/s(01)/) {
      $turnout1 = $$ButtonData{$1}{'Turnout1'};
      $turnout2 = $$ButtonData{$1}{'Turnout2'};
      $result = &DisplayDebug(1, "MidwayTrack, turnout1: $turnout1   " .
                                 "turnout2: $turnout2");

      # A button press unlocks the turnout.
      if ($$TrackData{$turnout1}{'Locked'} == 1) {
         $$TrackData{$turnout1}{'Locked'} = 0;
         $$TrackData{$turnout1}{'ManualSet'} = 0;
         $result = &DisplayMessage("MidwayTrack, $turnout1 is unlocked.");
         $result = &PlaySound("Unlock.wav");
         return 0;
      }

      # Ignore the button if a $turnout1 timeout or move is inprogress.
      return 0 if (&CheckTurnout($turnout1, 'MidwayTrack', $TurnoutData, 
                                 $TrackData));

      # Unlock and reposition the opposite turnout if currently locked.
      if ($$TrackData{$turnout2}{'Locked'} == 1) {

         # Ignore the button if a $turnout2 timeout or move is inprogress.
         return 0 if (&CheckTurnout($turnout2, 'MidwayTrack', $TurnoutData, 
                                    $TrackData));

         # Move $turnout2 only if necessary.
         if ($$TurnoutData{$turnout2}{'Pos'} ne 
            $$TurnoutData{$turnout2}{ $$TrackData{$turnout2}{'Inactive'} }) {

            $moveResult = &MoveTurnout($$TrackData{$turnout2}{'Inactive'}, 
                                       $turnout2, $TurnoutData);
            if ($moveResult == 1) {
               $result = &DisplayError("MidwayTrack, Failed to set " .
                                       "turnout $turnout2 to " .
                                       "$$TrackData{$turnout2}{'Inactive'}.");
               $result = &PlaySound("GE.wav");
               return 0;
            }
         }
         $$TrackData{$turnout2}{'Locked'} = 0;
         $$TrackData{$turnout2}{'ManualSet'} = 0;
         $result = &DisplayMessage("MidwayTrack, $turnout2 is unlocked.");
      }

      # Toggle turnout1 position.
      $$TrackData{$turnout1}{'ManualSet'} = 1;
      if ($$TurnoutData{$turnout1}{'Pos'} == $$TurnoutData{$turnout1}{'Open'}) {
         $position = 'Close';
      }
      else {
         $position = 'Open';
      }

      $moveResult = &MoveTurnout($position, $turnout1, $TurnoutData);
      if ($moveResult == 1) {
         $result = &DisplayError("MidwayTrack, Failed to set turnout " .
                                 "$turnout1 to $position");
         $result = &PlaySound("GE.wav");
      }
      else {
         $result = &DisplayMessage("MidwayTrack, $turnout1 set to $position.");
         $result = &PlaySound("A_.wav");
      }
   }
   return 0;
}

# =============================================================================
# FUNCTION:  WyeTrack
#
# DESCRIPTION:
#    This routine processes the user buttons associated with the T07 turnout.
#    These buttons, 02 and 03, are used to manually set the turnout position 
#    which selects the yard approach track to be used.
#
# CALLING SYNTAX:
#    $result = &WyeTrack($ButtonInput, \%ButtonData, \%TurnoutData, \%TrackData);
#
# ARGUMENTS:
#    $ButtonInput        User entered button input, if any.
#    $ButtonData         Pointer to %ButtonData hash.
#    $TurnoutData        Pointer to %TurnoutData hash.
#    $TrackData          Pointer to %TrackData hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub WyeTrack {
   my($ButtonInput, $ButtonData, $TurnoutData, $TrackData) = @_;
   my($result, $moveResult, $button, $turnout, $position);
   my(@buttonList) = split(',', $Button);

   $result = &DisplayDebug(2, "WyeTrack entry ...   ButtonInput: '$ButtonInput'");

   # ----------------------------------
   # Process single press button input.
   if ($ButtonInput =~ m/s(02)/ or $ButtonInput =~ m/s(03)/) {
      $button = $1;
      $turnout = $$ButtonData{$button}{'Turnout'};
      $result = &DisplayDebug(0, "WyeTrack, button: $button   turnout: $turnout");

      # Ignore the button if turnout move or train transit is inprogress.
      return 0 if (&CheckTurnout($turnout, 'WyeTrack', $TurnoutData, $TrackData));

      if ($button eq '02') {
         $position = 'Open';
      }
      else {
         $position = 'Close';
      }
            
      # Move turnout if necessary.
      if ($$TurnoutData{$turnout}{'Pos'} ne $$TurnoutData{$turnout}{$position}) {
         $moveResult = &MoveTurnout($position, $turnout, $TurnoutData);
         if ($moveResult == 1) {
            $result = &DisplayError("WyeTrack, Failed to set turnout " .
                                    "$turnout to $position");
            $result = &PlaySound("GE.wav");
         }
         else {
            $result = &DisplayMessage("WyeTrack, turnout $turnout set to ".
                                      "$position.");
            $result = &PlaySound("A_.wav");
         }
      }
      else {
         $result = &DisplayMessage("WyeTrack, turnout $turnout already at ".
                                      "$position.");
         $result = &PlaySound("A_.wav");
      }
   }
   return 0;
}

# =============================================================================
# FUNCTION:  CheckTurnout
#
# DESCRIPTION:
#    This routine is shared code used by MidwayTrack and WyeTrack to check for 
#    an inprogress turnout operation. This check is performed as part of button 
#    input processing. Warning tone and console message is output if necessary.
#
# CALLING SYNTAX:
#    $result = &CheckTurnout($Turnout, $Caller, \%TurnoutData, \%TrackData);
#
# ARGUMENTS:
#    $Turnout            Turnout number.
#    $Caller             Name of calling routine.
#    $TurnoutData        Pointer to %TurnoutData hash.
#    $TrackData          Pointer to %TrackData hash.
#
# RETURNED VALUES:
#    0 = no inprogress operation,  1 = inprogress operation.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub CheckTurnout {
   my($Turnout, $Caller, $TurnoutData, $TrackData) = @_;
   my($result);

   if ($$TurnoutData{$Turnout}{'Pid'} != 0) {
      $result = &DisplayMessage("$Caller, turnout $Turnout position change is ". 
                                "inprogress.");
      $result = &PlaySound("GE.wav");
      return 1; 
   }
   if ($$TrackData{$Turnout}{'Timeout'} > time) {
      $result = &DisplayMessage("$Caller, train transit of turnout " .
                                "$Turnout is inprogress.");
      $result = &PlaySound("GE.wav");
      return 1; 
   }
   return 0;
}

return 1;
