# =============================================================================
# FILE: DnB_Turnout.pm                                               6/22/2019
#
# SERVICES:  DnB TURNOUT FUNCTIONS
#
# DESCRIPTION:
#    This perl module provides turnout related functions used by the DnB model 
#    railroad control program.
#
# PERL VERSION: 5.24.1
#
# =============================================================================

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

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

@EXPORT = qw(
   I2C_InitServoDriver
   ProcessTurnoutFile
   InitTurnouts
   MoveTurnout
   SetTurnoutPosition
   TestTurnouts
);

use DnB_Message;
use Forks::Super;

# =============================================================================
# FUNCTION:  I2C_InitServoDriver
#
# DESCRIPTION:
#    This routine initializes the turnout servo I2C driver boards on the DnB 
#    model railroad. It sets parameters that are common to all servo ports. The 
#    Adafruit 16 Channel Servo Driver utilizes the PCA9685 chip. The pre_scale 
#    calculation is from the PCA9685 documentation.
#
#    Initialization sequence.
#       1. Get current ModeReg1.
#       2. Put PCA9685 into sleep mode. 
#       3. Set servo refresh rate.
#       4. Normal mode + register auto increment.
#       5. Put PCA9685 into normal mode. 
#
# CALLING SYNTAX:
#    $result = &I2C_InitServoDriver($BoardNmbr, $I2C_Address);
#
# ARGUMENTS:
#    $BoardNmbr      Drive board number being initialized.
#    $I2C_Address    I2C Address 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub I2C_InitServoDriver {

   my($BoardNmbr, $I2C_Address) = @_;
   my($result, $driver, $mode_data);

   my($minAddr, $maxAddr) = (0x40, 0x7F);  # AdaFruit 16 Channel PWM board range.
   my(%PCA9685) = ('ModeReg1' => 0x00, 'ModeReg2' => 0x01, 'AllLedOffH' => 0xFD,
                   'PreScale' => 0xFE);
   my($normal_mode) = 0xEF;   my($sleep_mode) = 0x10;   my($auto_inc) = 0xA1;

   my($freq) = 105;   # Refresh rate; 105 = 300-900 SG90 min/max position.
 
   my($pre_scale) = int((25000000.0 / (4096 * $freq)) - 1);

   $result = &DisplayDebug(2, "I2C_InitServoDriver, BoardNmbr: $BoardNmbr   " .
                           "I2C_Address: $I2C_Address   pre_scale: $pre_scale");

# Validate that address is within the Adafruit 16-channel driver range.
   if ($I2C_Address >= $minAddr and $I2C_Address <= $maxAddr) {
      $driver = RPi::I2C->new($I2C_Address);
      unless ($driver->check_device($I2C_Address)) {
         $result = &DisplayError("I2C_InitServoDriver, Failed to initialize " .
                              "I2C address: " . sprintf("0x%.2x",$I2C_Address));
         return 1;
      }
      $driver->write_byte(0x10, $PCA9685{'AllLedOffH'});  # Orderly shutdown.
      select (undef, undef, undef, .01);         # Wait for channels to stop.
      $mode_data = $driver->read_byte($PCA9685{'ModeReg1'});
      $driver->write_byte(($mode_data | $sleep_mode), $PCA9685{'ModeReg1'});
      $driver->write_byte($pre_scale, $PCA9685{'PreScale'});
      $mode_data = ($mode_data & $normal_mode) | $auto_inc;
      $driver->write_byte(($mode_data), $PCA9685{'ModeReg1'});
      $result = &DisplayDebug(2, "I2C_InitServoDriver, PreScale: " .
                                 $driver->read_byte($PCA9685{'PreScale'}));
      undef($driver);
   }
   else {
      $result = &DisplayError("I2C_InitServoDriver, Invalid I2C address: " .
                              "$I2C_Address   Board: $BoardNmbr");
      return 1;
   }
   return 0;
}

# =============================================================================
# FUNCTION:  ProcessTurnoutFile
#
# DESCRIPTION:
#    This routine reads or writes the specified turnout data file. Used to
#    retain turnout operational data between program starts.
#
# CALLING SYNTAX:
#    $result = &ProcessTurnoutFile($FileName, $Function, \%TurnoutData);
#
# ARGUMENTS:
#    $FileName       File to Read/Write
#    $Function       "Read" or "Write"
#    $TurnoutData    Pointer to %TurnoutData hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub ProcessTurnoutFile {

   my($FileName, $Function, $TurnoutData) = @_;
   my($result, $rec, $key, $turnout);
   my(@fileData) = ();

   my(@keyList) = ('Pid','Addr','Port','Pos','Rate','Open','Middle','Close',
                   'MinPos','MaxPos','Id');
   
   $result = &DisplayDebug(2, "ProcessTurnoutFile, Function: $Function   " .
                              "keyList: '@keyList'");
   
   if ($Function =~ m/^Read$/i) {
      if (-e $FileName) {
         if (&ReadFile($FileName, \@fileData)) {
            $result = &DisplayWarning("ProcessTurnoutData, Using default " .
                                      "turnout data.");
         }
         else {
            %$TurnoutData = ();
            foreach $rec (@fileData) {
               next if ($rec =~ m/^\s*$/ or $rec =~ m/^#/);
               if ($rec =~ m/Turnout:\s*(\d+)/i) {
                  $turnout = sprintf("%2s",$1);
                  $$TurnoutData{$turnout}{'Pid'} = 0;
                  foreach $key (@keyList) {
                     if ($key eq 'Id') {
                        if ($rec =~ m/$key:(.+)/) {
                           $$TurnoutData{$turnout}{$key} = &Trim($1);
                        }
                        else {
                           $result = &DisplayWarning("ProcessTurnoutData, " .
                                        "'$key' not found: '$rec'");
                           next;
                        }
                     }
                     else {
                        if ($rec =~ m/$key:\s*(\d+)/) {
                           $$TurnoutData{$turnout}{$key} = $1;
                        }
                        else {
                           $result = &DisplayWarning("ProcessTurnoutData, " .
                                        "'$key' not found: '$rec'");
                           next
                        }
                     }
                     $result = &DisplayDebug(2, "ProcessTurnoutFile, " .
                                  "Turnout: $turnout   key: $key   value: " .
                                  "$$TurnoutData{$turnout}{$key}");
                  }
               }
               else {         
                  $result = &DisplayWarning("ProcessTurnoutData, 'Turnout' key " .
                                            "not found: '$rec'");
               }   
            }
         }
         $rec = scalar keys %$TurnoutData;
         $result = &DisplayDebug(1, "ProcessTurnoutFile, Function: $Function " .
                                    "$rec turnout records.");
      }
      else {
         $result = &DisplayWarning("ProcessTurnoutData: File not found: " .
                                   "$FileName."); 
         $result = &DisplayWarning("ProcessTurnoutData: Using default " .
                                   "turnout data.");
      }
   }
   elsif ($Function =~ m/^Write$/i) {
      push (@fileData, "# ===============================================");
      push (@fileData, "# Turnout data file. Loaded during program start.");
      push (@fileData, "# Edited values will be used upon next start. See");
      push (@fileData, "# DnB.pl 'Turnout Related Data' section for more ");
      push (@fileData, "# information.");
      push (@fileData, "# ===============================================");

      $rec = scalar keys %$TurnoutData;
      $result = &DisplayDebug(1, "ProcessTurnoutFile, Function: $Function $rec " .
                                 "turnout records.");
      
      foreach $turnout (sort keys %$TurnoutData) {
         $rec = join(":", "Turnout", $turnout);
         $$TurnoutData{$turnout}{'Pid'} = 0;
         foreach $key (@keyList) {
            $rec = join(" ", $rec, join(":", $key, 
                        $$TurnoutData{$turnout}{$key}));
         }   
         push (@fileData, $rec);
         $result = &DisplayDebug(2, "ProcessTurnoutFile, $Function: $rec");
      }
      $result = &WriteFile($FileName, \@fileData);
   }
   else {
      $result = &DisplayWarning("ProcessTurnoutData, Unsupported function: " .
                                "$Function");
   }
   return 0;
}

# =============================================================================
# FUNCTION:  InitTurnouts
#
# DESCRIPTION:
#    Called once during DnB startup, this routine initializes all turnouts to 
#    the PWM position specified in %TurnoutData. This ensures that all servo 
#    driver board channels are synchronized to the %TurnoutData specified PWM
#    position. 
#
#    If optional data is specified, the servo is set to the specified PWM 
#    position. This position is used for physical turnout point adjustment.
#
# CALLING SYNTAX:
#    $result = &InitTurnouts(\%ServoBoardAddress, \%TurnoutData, $Turnout,
#                            $Position);
#
# ARGUMENTS:
#    $ServoBoardAddress      Pointer to %ServoBoardAddress hash.
#    $TurnoutData            Pointer to %TurnoutData hash.
#    $Turnout                Optional; turnout to position. 
#    $Position               Optional; position to set.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub InitTurnouts {
   my($ServoBoardAddress, $TurnoutData, $Turnout, $Position) = @_;
   my($result, $board, $tNmbr);
   my($fail) = 0;
   
   $result = &DisplayDebug(2, "InitTurnouts, $Turnout: $Turnout   $Position: " .
                              "'$Position'");

   if ($Turnout ne '') {
      $Turnout = "0${Turnout}" if (length($Turnout) == 1);
   }
   if ($Position ne 'Open' and $Position ne 'Close') {
      $Position = 'Middle';
   }

   for ($board = 1; $board <= scalar keys(%$ServoBoardAddress); $board++) {
      if ($$ServoBoardAddress{$board} == 0) {
         $result = &DisplayDebug(1, "InitTurnouts, Skip board $board " .
                                    "I2C_Address 0, code debug.");
         next;
      }
      $result = &DisplayMessage("Initializing turnout I2C board $board ...");
      return 1 if (&I2C_InitServoDriver($board, $$ServoBoardAddress{$board}));

      $result = &DisplayMessage("Initializing turnout positions on board " .
                                "$board ...");

      foreach $tNmbr (sort keys %$TurnoutData) {
         if ($$TurnoutData{$tNmbr}{'Addr'} == $$ServoBoardAddress{$board}) {
            if ($Turnout eq '00' or $Turnout eq $tNmbr) {
               $$TurnoutData{$tNmbr}{'Pos'} = $$TurnoutData{$tNmbr}{$Position};
            }
            elsif ($$TurnoutData{$tNmbr}{'Pos'} != $$TurnoutData{$tNmbr}{'Open'} 
                   and $$TurnoutData{$tNmbr}{'Pos'} != $$TurnoutData{$tNmbr}
                   {'Close'}) {
               $$TurnoutData{$tNmbr}{'Pos'} = $$TurnoutData{$tNmbr}{'Middle'};
            }

            if (&SetTurnoutPosition($$TurnoutData{$tNmbr}{'Pos'}, $tNmbr, 
                                    $TurnoutData)) {
               $result = &DisplayWarning("InitTurnouts, Failed to set " .
                                 "turnout.   board $board   Turnout: $tNmbr" .
                                 "Position: $$TurnoutData{$tNmbr}{'Pos'}");
               $fail = 1;
            }

            $$TurnoutData{$tNmbr}{'Pid'} = 0;  # Ensure the Pid value is 0.
            select (undef, undef, undef, .1);  # Delay so we don't overtax
                                               # the servo power supply.
         }
      }
      $result = &DisplayMessage("All board $board turnouts initialized.");
   }
   if ($Turnout ne '') {
      if ($Turnout eq '00') {
         $result = &DisplayMessage("All turnouts set to $Position position.");
      }
      else {
         $result = &DisplayMessage("Turnout $Turnout set to $Position " .
                                   "position.");
      }
   }
   return 1 if ($fail == 1);    # Error return if failure.
   return 0;
}

# =============================================================================
# FUNCTION:  MoveTurnout
#
# DESCRIPTION:
#    This routine moves the turnout servo using the specified data. It is used 
#    to perform a slow motion position change. This is done by forking to a
#    child process and calling SetTurnoutPosition 50 times a second until the 
#    move is complete. Each call positions the turnout servo toward the final 
#    position by a move step amount ('Rate'/50). Once the move is completed, 
#    the turnout position is updated in the TurnoutData hash and the child 
#    exits. A 'Rate' value of 450 positions the turnout from Open (350) to 
#    Close (850) in about 1.1 seconds.
#
#    Note that this routine checks the Pwm and Rate values to provide some 
#    servo protection. 
#
# CALLING SYNTAX:
#    $result = &MoveTurnout($Function, $TurnoutNmbr, \%TurnoutData);
#
# ARGUMENTS:
#    $Function       'Open', 'Middle', or 'Close'.
#    $TurnoutNmbr    Turnout number.
#    $TurnoutData    Pointer to TurnoutData hash. 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error, 2 = Already in position.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub MoveTurnout {
   my($Function, $TurnoutNmbr, $TurnoutData) = @_;
   my($result, $pwmCurrent, $pwmFinal, $moveRate, $moveStep, $pid);
   my($timeout) = 40;    # Wait 10 seconds (40/.25) for move to complete.

   $result = &DisplayDebug(2, "MoveTurnout, Entry ...   $Function $TurnoutNmbr");

   if ($TurnoutNmbr ne "") {
      if ($Function =~ m/Open/i) {
         $pwmFinal = $$TurnoutData{$TurnoutNmbr}{'Open'};
      }
      elsif ($Function =~ m/Middle/i) {
         $pwmFinal = $$TurnoutData{$TurnoutNmbr}{'Middle'};
      }
      elsif ($Function =~ m/Close/i) {
         $pwmFinal = $$TurnoutData{$TurnoutNmbr}{'Close'};
      }
      else {
         $result = &DisplayError("MoveTurnout, invalid function: '$Function'");
         return 1;
      }

      # Check and wait for turnout to be idle.
      while ($$TurnoutData{$TurnoutNmbr}{'Pid'} > 0 and $timeout > 0) {
         if (($timeout % 4) == 0) {
            $result = &DisplayDebug(2, "MoveTurnout, waiting for previous move " .
                                    "to complete. timeout: $timeout   Pid: " .
                                    "$$TurnoutData{$TurnoutNmbr}{'Pid'}   Pos: " .
                                    "$$TurnoutData{$TurnoutNmbr}{'Pos'}");
         }
         $timeout--;
         select (undef, undef, undef, .25);  # Wait quarter sec.
      }
      if ($$TurnoutData{$TurnoutNmbr}{Pid} > 0) {
         $result = &DisplayError("MoveTurnout, Turnout $TurnoutNmbr, Previous " .
                                 "move still in progress, pid: " .
                                 "$$TurnoutData{$TurnoutNmbr}{'Pid'}.");

         # Check if the process is running, $result == 0. If so, kill it.
         # Cleanup state data and continue new turnout move.
         $result = waitpid($$TurnoutData{$TurnoutNmbr}{'Pid'}, WNOHANG);
         system("kill -9 $$TurnoutData{$TurnoutNmbr}{'Pid'}") if ($result == 0);
         $$TurnoutData{$TurnoutNmbr}{'Pid'} = 0;
      }

      $pwmCurrent = $$TurnoutData{$TurnoutNmbr}{'Pos'};
      if ($pwmCurrent == $pwmFinal) {           # Done if already in position.
         $result = &DisplayDebug(2, "MoveTurnout, $TurnoutNmbr already in " .
                                    "requested position: $pwmFinal");
         return 2;
      }
      $moveRate = $$TurnoutData{$TurnoutNmbr}{'Rate'};      

      # Make sure move does not exceed limits.
      $pwmFinal = $$TurnoutData{$TurnoutNmbr}{'MinPos'} 
                  if ($pwmFinal < $$TurnoutData{$TurnoutNmbr}{'MinPos'});
      $pwmFinal = $$TurnoutData{$TurnoutNmbr}{'MaxPos'} 
                  if ($pwmFinal > $$TurnoutData{$TurnoutNmbr}{'MaxPos'});

      if ($moveRate > 0) {
         # Fork program to complete the move. Use Forks::Super which is a go 
         # between the parent and child. It has a function for writing child 
         # data back to the main program using child STDOUT and STDERR. It is 
         # not necessary to 'reap' the child when using Forks::Super. Also, 
         # SIG{CHILD} should not be set by this program. It is set/used by 
         # Forks::Super. Do no other printing, including debug output.
         #
         # STDERR: move complete. $TurnoutData{<tNmbr>}{'Pid'} set to 0.       
         # STDOUT: new turnout position. $TurnoutData{<tNmbr>}{'Pos'}.       

         $result = &DisplayDebug(2, "MoveTurnout, pre-fork: $Function " .
                                    "$TurnoutNmbr   pwmCurrent: $pwmCurrent" .
                                    "   pwmFinal: $pwmFinal" .
                                    "   moveRate: $moveRate");

         $pid = fork { os_priority => 1, 
                       stdout => \$$TurnoutData{$TurnoutNmbr}{'Pos'}, 
                       stderr => \$$TurnoutData{$TurnoutNmbr}{'Pid'} };
         if (!defined($pid)) {
            $result = &DisplayError("TurnoutChildProcess, Failed to create " .
                                    "child process. $!");
            return 1;
         }
#----------
         elsif ($pid == 0) {          # fork returned 0, so this is the child
            $moveStep = int($moveRate/50) +1;      # Step increment
            while ($pwmCurrent != $pwmFinal) {
               if ($pwmCurrent < $pwmFinal) {      # Determine move direction
                  $pwmCurrent += $moveStep;
                  $pwmCurrent = $pwmFinal if ($pwmCurrent > $pwmFinal);
               }
               else {
                  $pwmCurrent -= $moveStep;
                  $pwmCurrent = $pwmFinal if ($pwmCurrent < $pwmFinal);
               }
               
               if (&SetTurnoutPosition($pwmCurrent, $TurnoutNmbr, $TurnoutData)) {
                  # Retain previous pwmCurrent in Pos if error is returned.
                  print STDERR 0;            # Clear Pid, move has completed.
                  exit(1);                   # Starting position is retained.
               }
               select (undef, undef, undef, .02);
            }
            print STDOUT $pwmCurrent;        # Store position of turnout
            print STDERR 0;                  # Clear Pid, move has completed.
            exit(0);
         }
#----------
         $$TurnoutData{$TurnoutNmbr}{'Pid'} = $pid;  # Parent: Move in-progress.
         $result = &DisplayDebug(1, "MoveTurnout, $Function $TurnoutNmbr " .
                           "forked pid: $$TurnoutData{$TurnoutNmbr}{'Pid'}");
      }
      else {   
         $result = &DisplayWarning("MoveTurnout, Rate value must be greater " .
                                   "than 0.");
         return 1;
      }
   }
   else {
      $result = &DisplayError("MoveTurnout, invalid turnout number: " .
                              "$TurnoutNmbr");
      return 1;
   }
   return 0;
}

# =============================================================================
# FUNCTION:  SetTurnoutPosition
#
# DESCRIPTION:
#    This routine sets the turnout servo using the specified data. This 
#    routune writes the I2C interface with the needed command bytes.
#
# CALLING SYNTAX:
#    $result = &SetTurnoutPosition($Position, $TurnoutNmbr, \%TurnoutData);
#
# ARGUMENTS:
#    $Position       PWM position to set.
#    $TurnoutNmbr    Turnout number.
#    $TurnoutData    Pointer to TurnoutData hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub SetTurnoutPosition {
   my($Position, $TurnoutNmbr, $TurnoutData) = @_;
   my($result, $driver, $reg_start, $reg_data_on, $reg_data_off);
   my(@data) = (); 

   $result = &DisplayDebug(2, "SetTurnoutPosition, $TurnoutNmbr - $Position");

   if ($TurnoutNmbr ne "") {
      if ($Position >= $$TurnoutData{$TurnoutNmbr}{'MinPos'} and 
          $Position <= $$TurnoutData{$TurnoutNmbr}{'MaxPos'}) {      
         $reg_start = (($$TurnoutData{$TurnoutNmbr}{'Port'} % 16) * 4) + 6;

         # Stager pulse start (* 10) to minimuze power drops.
         $reg_data_on = $$TurnoutData{$TurnoutNmbr}{'Port'} * 10;
         push (@data, ($reg_data_on & 0xFF));         # on_L
         push (@data, (($reg_data_off >> 8) & 0x0F)); # on_H
         $reg_data_off = $reg_data_on + $Position;
         push (@data, ($reg_data_off & 0xFF));        # off_L
         push (@data, (($reg_data_off >> 8) & 0x0F)); # off_H

         $driver = RPi::I2C->new($$TurnoutData{$TurnoutNmbr}{'Addr'});
         unless ($driver->check_device($$TurnoutData{$TurnoutNmbr}{'Addr'})) {
            $result = &DisplayError("SetTurnoutPosition, Failed to initialize " .
                         "I2C address: " . 
                         sprintf("%.2x",$$TurnoutData{$TurnoutNmbr}{'Addr'}));
            return 1;
         }
         $driver->write_block(\@data, $reg_start);
         undef($driver);
      }
      else {
         $result = &DisplayError("SetTurnoutPosition, Out of range Pwm value, " .
                                 "Turnout: $TurnoutNmbr  Position: $Position");
         return 1;
      }
   }
   else {
      $result = &DisplayError("SetTurnoutPosition, invalid turnout number: " .
                              "$TurnoutNmbr");
      return 1;
   }
   return 0;
}

# =============================================================================
# FUNCTION:  TestTurnouts
#
# DESCRIPTION:
#    This routine cycles the specified turnout range between the open and 
#    closed positions.
#
# CALLING SYNTAX:
#    $result = &TestTurnouts($Range, \%TurnoutData);
#
# ARGUMENTS:
#    $Range           Turnout number or range to use.
#    $TurnoutData     Pointer to TurnoutData hash. 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    $main::MainRun
# =============================================================================
sub TestTurnouts {

   my($Range, $TurnoutData) = @_;
   my($result, $turnout, $start, $end, $nmbr, $oper, $pid, $cnt, $key,
      @turnoutNumbers, @inProgress, $position);
   my($cntTurnout) = scalar keys %$TurnoutData;
   my(%operation) = (1 => 'Open ', 2 => 'Close');
   my(@turnoutList) = ();
   my($random, $wait) = (0, 0);

   $result = &DisplayDebug(1, "TestTurnouts, Entry ... Range: '$Range'   " .
                              "cntTurnout: $cntTurnout");

   # ==============================
   # Set specified position and exit.
   
   if ($Range =~ m/^(Open):(\d+)/i or $Range =~ m/^(Close):(\d+)/i or 
       $Range =~ m/^(Middle):(\d+)/i) {
      $position = ucfirst(lc $1);
      $turnout = $2;
      $turnout = "0${turnout}" if (length($turnout) == 1);

      # The %TurnoutData Id string must contain the word turnout.
      if ($$TurnoutData{$turnout}{'Id'} =~ m/turnout/) {
         $result = &MoveTurnout($position, $turnout, $TurnoutData);
         $result = &DisplayMessage("Turnout $turnout set to '$position'.");
      }
      else {
         $result = &DisplayError("TestTurnouts, invalid turnout number: " .
                                 "$turnout");
      }
      exit(0);
   }
   elsif ($Range =~ m/^(Open)$/i or $Range =~ m/^(Close)$/i or 
          $Range =~ m/^(Middle)$/i) {
      $position = ucfirst(lc $1);

      # The %TurnoutData Id string must contain the word turnout.
      foreach $turnout (sort keys %$TurnoutData) {
         if ($$TurnoutData{$turnout}{'Id'} =~ m/turnout/) {
            $result = &MoveTurnout($position, $turnout, $TurnoutData);
            $result = &DisplayDebug(1, "TestTurnouts, turnout: " .
                                       "$turnout set to $position");
         }
      }
      $result = &DisplayMessage("All turnouts set to '$position'.");
      exit(0);
   }

   # ==============================
   # Process special modifiers and then setup for looped testing.
   
   if ($Range =~ m/r/i) {
      $random = 1;
      $Range =~ s/r//i;
   }
   if ($Range =~ m/w/i) {
      $wait = 1;
      $Range =~ s/w//i;
   }
   
   if ($Range =~ m/(\d+):(\d+)/) {   # Range specified.
      $start = $1;
      $end = $2;
      if ($start > $end or $start <= 0 or $start > $cntTurnout or $end <= 0 or 
          $end > $cntTurnout) {
         $result = &DisplayError("TestTurnouts, invalid turnout range: '$Range'" .
                                 "   cntTurnout: $cntTurnout");
         return 1;
      }
      for ($turnout = $start; $turnout <= $end; $turnout++) {
         push (@turnoutList, $turnout);
      }
   }
   else {
      @turnoutList = split(",", $Range);
   }
   $result = &DisplayDebug(1, "TestTurnouts, random: $random   wait: $wait   " .
                              "turnoutList: '@turnoutList'");

   # Identify the servos being used for turnouts. The %TurnoutData Id string 
   # must contain the word turnout.
   foreach $key (sort keys %$TurnoutData) { 
      if ($$TurnoutData{$key}{'Id'} =~ m/turnout/) {
         push (@turnoutNumbers, $key);
      }
   }

   $oper = 'Open  ';
   while ($main::MainRun) {
      # For random testing, we randomize the turnoutNumbers list and also the
      # Open/Close operation. For non-random, Open and then Close the turnouts
      # in the specified order.
      $result = &ShuffleArray(\@turnoutNumbers) if ($random == 1);

      foreach $turnout (@turnoutNumbers) {
         return 0 unless ($main::MainRun);
         $nmbr = $turnout;
         $nmbr =~ s/^0//;
         if (grep /^$nmbr$/, @turnoutList) {  # Move turnout if on the list.
            $oper = $operation{(int(rand(2))+1)} if ($random == 1);
            if ($#inProgress < 0) {
               $result = &DisplayMessage("TestTurnouts, $oper $turnout   " .
                                         "Concurrent moves: none");
            }
            else {
               $result = &DisplayMessage("TestTurnouts, $oper $turnout   " .
                                         "Concurrent moves: @inProgress");
            }
            $result = &MoveTurnout($oper, $turnout, $TurnoutData);
            return 1 if ($result == 1);
            if ($result == 2) {
               $result = &DisplayDebug(2, "TestTurnouts, MoveTurnout " .
                                          "$turnout returned already " .
                                          "in position."); 
            }
            elsif ($result == 0) {
               if ($wait == 1) {
                  $cnt = 20;
                  while ($$TurnoutData{$turnout}{'Pid'}) {
                     if ($cnt == 0) {
                        $result = &DisplayError("TestTurnouts, timeout " .
                           "waiting for turnout $turnout to complete " .
                           "positioning.");
                        return 1;
                     }
                     $result = &DisplayDebug(2, "TestTurnouts, waiting for " .
                                       "pid: $$TurnoutData{$turnout}{'Pid'}");
                     select (undef, undef, undef, .5);
                     $cnt--;
                  }
                  $result = &DisplayDebug(2, "TestTurnouts, Turnout " .
                                 "$turnout new position: " .
                                 "$$TurnoutData{$turnout}{'Pos'}"); 
               } 
            }
            @inProgress = ();
            foreach $key (sort keys(%$TurnoutData)) {
               push (@inProgress, $key) if ($$TurnoutData{$key}{'Pid'} != 0);
            }
            select (undef, undef, undef, .05) unless ($result == 2); 
         }
      }

      if ($random == 0) {   # Change if doing sequential testing.
         if ($oper =~ m/Open/) {
            $oper = 'Close ';
         }
         else {
            $oper = 'Open  ';
         }
      }
      sleep 1;
   }
   return 0;
}

return 1;
