# ============================================================================
# FILE: DnB_GradeCrossing.pm                                       10/26/2018
#
# SERVICES:  DnB GRADE CROSSING FUNCTIONS
#
# DESCRIPTION:
#    This perl module provides grade crossing related functions used by the 
#    DnB model railroad control program.
#
# PERL VERSION: 5.24.1
#
# =============================================================================

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

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

@EXPORT = qw(
   ProcessGradeCrossing
   GcChildProcess
   TestGradeCrossing
);

use DnB_Sensor;
use DnB_Signal;
use DnB_Turnout;
use DnB_Message;
use Forks::Super;

# =============================================================================
# FUNCTION:  ProcessGradeCrossing
#
# DESCRIPTION:
#    This routine is used to process the specified grade crossing. It is called
#    once an iteration by the main program loop. State data that is used for 
#    grade crossing control is persisted in the %GradeCrossingData hash. Each 
#    grade crossing is in one of the following states; 'idle', 'gateLower', 
#    'approach', 'road', 'gateRaise' or 'depart'. %GradeCrossingData values, 
#    sensor bits, and code within this routine, transition the signal through 
#    these states. Operation is as follows.
#
#    1. Configuration and initializations set in %GradeCrossingData hash.
#
#    2. In 'idle' state, a train approaching the grade crossing is detected by 
#    sensors 'AprEast', 'AprWest', or 'Road'. This causes the signals to begin 
#    flashing. 'SigRun' is set to 'on'. 'GateDelay' is set and the state 
#    transitions to 'gateLower'.
#
#    3. In 'gateLower' state, if grade crossing does not have gates, 'AprTimer'
#    is set and the state transitions to 'approach'. Otherwise, after 'GateDelay'
#    time, the gates are lowered, 'AprTimer' is set, and the state transitions 
#    to 'approach'. This delay time results in better simulation of proto-typical 
#    signal operation.
#
#    4. In 'approach' state, if 'road' state is not achieved before 'AprTimer' 
#    expires, the code transitions to the 'gateRaise' state. This could occur if 
#    the train stops or backs away before reaching the 'Road' sensor. An active 
#    'Road' sensor causes transition to the 'road' state.
#
#    5. In 'road' state, a short timeout is set into 'RoadTimer'. Additional 
#    'Road' sensor activity reloads this timer. This maintains 'road' state 
#    while the train occupies the grade crossing. When no further 'Road' sensor 
#    activity is reported, 'RoadTimer' will expire. The state transitions to
#    'gateRaise'.
#
#    6. In 'gateRaise' state, if grade crossing does not have gates, 'DepTimer'
#    is set and the state transitions to 'depart'. Otherwise, the gates are 
#    raised. Once completed (servo pid == 0), 'DepTimer' is set and the state 
#    transitions to 'depart'.
#
#    7. In the 'depart' state, the signal lamp flashing is stopped and 'SigRun' 
#    is set to 'off'. Outbound train 'AprEast' or 'AprWest' sensor activity 
#    restarts the 'DepTimer' maintaining the 'depart' state. Once the last car 
#    of the outbound train is past the 'AprEast' or 'AprWest' sensor, the 
#    'DepTimer' expires and the state transitions to 'idle'. 
#
#    If the train backs up, 'Road' sensor activity will immediately transition
#    the state to 'idle'. From 'idle', the active 'Road' sensor will immediately
#    start a new signaling cycle. 
#
# CALLING SYNTAX:
#    $result = &ProcessGradeCrossing($gc, \%GradeCrossingData, \%SensorBit,
#              \%TurnoutData, \%MCP23017, \%SensorState);
#
# ARGUMENTS:
#    $Gc                 Index to data in %GradeCrossingData.
#    $GradeCrossingData  Pointer to %GradeCrossingData hash.
#    $SensorBit          Pointer to %SensorBit hash.
#    $TurnoutData        Pointer to %TurnoutData hash. (needed for gates and sound)
#    $MCP23017           Pointer to %MCP23017 hash. (GPIO definitions)
#    $SensorState        Pointer to %SensorState hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub ProcessGradeCrossing {
   my($Gc, $GradeCrossingData, $SensorBit, $TurnoutData, $MCP23017, 
      $SensorState) = @_;
   my($result, @gates, $gate);

# Isolate the current grade crossing sensor bit values and get the current time.

   my($aprEastSensor) = &GetSensorBit($$GradeCrossingData{$Gc}{'AprEast'}, 
                                      $SensorBit, $SensorState);
   my($roadSensor) = &GetSensorBit($$GradeCrossingData{$Gc}{'Road'}, $SensorBit, 
                                   $SensorState);
   my($aprWestSensor) = &GetSensorBit($$GradeCrossingData{$Gc}{'AprWest'}, 
                                      $SensorBit, $SensorState);
   my($cTime) = time;

   $result = &DisplayDebug(2, "ProcessGradeCrossing $Gc, State: " .
             "$$GradeCrossingData{$Gc}{'State'}   aprEastSensor: $aprEastSensor" .
             "   roadSensor: $roadSensor   aprWestSensor: $aprWestSensor   " .
             "cTime: $cTime");

# Idle state code. ------------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'idle') {
      if ($roadSensor == 1 or $aprEastSensor == 1 or $aprWestSensor == 1) {
         if ($$GradeCrossingData{$Gc}{'SigRun'} ne 'on') {
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, '" .
                      $$GradeCrossingData{$Gc}{'State'} . 
                      "' start signals");

            # Start lamps and approach sound effect.
            Forks::Super::write_stdin($$GradeCrossingData{$Gc}{'Pid'}, 
               'start:apr');
         }
         
         $$GradeCrossingData{$Gc}{'SigRun'} = 'on';
         $$GradeCrossingData{$Gc}{'GateDelay'} = $cTime + 1;
         $$GradeCrossingData{$Gc}{'State'} = 'gateLower';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'idle' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
   }

# GateLower state code. --------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'gateLower') {

      # Lower gates if equipt else approach state.
      if ($$GradeCrossingData{$Gc}{'Gate'} eq "") {
         $$GradeCrossingData{$Gc}{'AprTimer'} = $cTime + 2;
         $$GradeCrossingData{$Gc}{'State'} = 'approach';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'gateLower' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
      elsif ($$GradeCrossingData{$Gc}{'GateDelay'} < $cTime) { # Delay time done?
         @gates = split(",", $$GradeCrossingData{$Gc}{'Gate'});
         foreach $gate (@gates) {
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, '" .
                      $$GradeCrossingData{$Gc}{'State'} . " state' close " .
                      "gate: $gate");
            $result = &MoveTurnout('Close', $gate, $TurnoutData);
         }
         $$GradeCrossingData{$Gc}{'AprTimer'} = $cTime + 2;
         $$GradeCrossingData{$Gc}{'State'} = 'approach';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'gateLower' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
   }

# Approach state code. --------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'approach') {
      if ($roadSensor == 1) {
         $$GradeCrossingData{$Gc}{'State'} = 'road';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'approach' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");

         # Change to roadside sound effect.        
         Forks::Super::write_stdin($$GradeCrossingData{$Gc}{'Pid'}, 'start:road');
      }
      elsif ($$GradeCrossingData{$Gc}{'AprTimer'} < $cTime) { # AprTimer timeout?
         if ($aprEastSensor == 1 or $aprWestSensor == 1) { # Sensor still active?
            $$GradeCrossingData{$Gc}{'AprTimer'} = $cTime + 2;
         }
         else {
            $$GradeCrossingData{$Gc}{'State'} = 'gateRaise';
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'approach' " .
                      "==> '$$GradeCrossingData{$Gc}{'State'}'.");
         }
      }
   }

# Road state code. ------------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'road') {
      if ($roadSensor == 1) {
         $$GradeCrossingData{$Gc}{'RoadTimer'} = $cTime + 2;    # Set RoadTimer
      }
      else {
         if ($$GradeCrossingData{$Gc}{'RoadTimer'} < $cTime) { # timeout?
            $$GradeCrossingData{$Gc}{'State'} = 'gateRaise';
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'road' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");

            # Set back to approach sound effect.
            Forks::Super::write_stdin($$GradeCrossingData{$Gc}{'Pid'}, 
            'start:apr');
         }
      }
   }

# GateRaise state code. --------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'gateRaise') {

      # Raise gates if equipt else depart state.
      if ($$GradeCrossingData{$Gc}{'Gate'} eq "") {
         $$GradeCrossingData{$Gc}{'DepTimer'} = $cTime + 1;  # Set DepTimer
         $$GradeCrossingData{$Gc}{'State'} = 'depart';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'gateRaise' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
      else {
         if ($$GradeCrossingData{$Gc}{'GateServo'} == 0) {
            @gates = split(",", $$GradeCrossingData{$Gc}{'Gate'});
            foreach $gate (@gates) {
               $result = &DisplayMessage("ProcessGradeCrossing $Gc, '" .
                         $$GradeCrossingData{$Gc}{'State'} . " state' open " .
                         "gate: $gate");
               $result = &MoveTurnout('Open', $gate, $TurnoutData);
            }
            $$GradeCrossingData{$Gc}{'GateServo'} = $gates[0];
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, '" .
                      $$GradeCrossingData{$Gc}{'State'} . " state' waiting for " .
                      "gate $$GradeCrossingData{$Gc}{'GateServo'} to open.");
         }
         elsif ($$TurnoutData{$$GradeCrossingData{$Gc}{'GateServo'}}{Pid} == 0) {
            $$GradeCrossingData{$Gc}{'GateServo'} = 0;
            $$GradeCrossingData{$Gc}{'DepTimer'} = $cTime + 1;  # Set DepTimer
            $$GradeCrossingData{$Gc}{'State'} = 'depart';
            $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'gateRaise' " .
                      "--> '$$GradeCrossingData{$Gc}{'State'}'.");
         }
      }
   }

# Depart state code. ----------------------------------------------------------
   if ($$GradeCrossingData{$Gc}{'State'} eq 'depart') {
      if ($$GradeCrossingData{$Gc}{'SigRun'} ne 'off') {
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, '" .
                                   $$GradeCrossingData{$Gc}{'State'} . 
                                   "' stop signals");
         Forks::Super::write_stdin($$GradeCrossingData{$Gc}{'Pid'}, 'stop');
         $$GradeCrossingData{$Gc}{'SigRun'} = 'off';
      }

      if ($roadSensor == 1) {
         $$GradeCrossingData{$Gc}{'State'} = 'idle';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'depart' ==> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
      elsif ($$GradeCrossingData{$Gc}{'DepTimer'} < $cTime) { # DepTimer timeout?
         $$GradeCrossingData{$Gc}{'State'} = 'idle';
         $result = &DisplayMessage("ProcessGradeCrossing $Gc, 'depart' --> " .
                      "'$$GradeCrossingData{$Gc}{'State'}'.");
      }
      elsif ($aprEastSensor == 1 or $aprWestSensor == 1) {
         $$GradeCrossingData{$Gc}{'DepTimer'} = $cTime + 1;   # Set DepTimer
      }      
   }

   return 0;
}

# =============================================================================
# FUNCTION:  GcChildProcess
#
# DESCRIPTION:
#    This routine is launched as a child process during main program startup
#    and is used to start and stop grade crossing signal lamp flash operation.
#    Since Forks::Super does not allow a child to fork to another child, any
#    servo driven gate timing and positioning for the signal must be done by 
#    the caller.
#
#    A dedicated GcChildProcess is started for each grade crossing. The returned
#    child Pid value is stored in the %GradeCrossingData hash. This Pid value 
#    is used in the Forks::Super::write_stdin message to send commands to the
#    proper GcChildProcess instance. 
#
# CALLING SYNTAX:
#    $pid = fork { os_priority => 1, sub => \&GcChildProcess, 
#                  child_fh => "in socket",
#                  args => [ $Gc, $SignalChildPid, \%GradeCrossingData ] };
#
#       $$GradeCrossing      The signal to be processed.
#       $SignalChildPid      PID of child signal refresh process.
#       $GradeCrossingData   Pointer to the %GradeCrossingData hash.
#
#    The SuperForks 'child_fh' functionality is used for communication between 
#    the parent and child processes. The parent sends a start/stop signal message
#    to the child's stdin. The message must be formatted as follows.
#
#       start:apr  - Start flashing lamps with bell sound 1.
#       start:road - Start flashing lamps with bell sound 2.
#       stop       - Stop lamp flash and bell sound.
#       exit       - Terminate GcChildProcess.
#
# SEND DATA TO CHILD:
#    Forks::Super::write_stdin($GcChildPid, 'start:apr'));
#    Forks::Super::write_stdin($GcChildPid, 'start:road'));
#    Forks::Super::write_stdin($GcChildPid, 'stop'));
#    Forks::Super::write_stdin($GcChildPid, 'exit'));
#
# RETURNED VALUES:
#    PID of child process = Success, 0 = Error
#
# ACCESSED GLOBAL VARIABLES:
#    $main::ChildName
# =============================================================================
sub GcChildProcess {
   my($GradeCrossing, $SignalChildPid, $GradeCrossingData) = @_;
   my($result, $x, @buffer, $lampColor, $snd);
   my(%sound) = ('apr' => 'Grn', 'road' => 'Red');   # Grn = 0b10, Red = 0b01
   my($cmd) = "";   my($lampFlash) = 0;
   my($signalNmbr) = $$GradeCrossingData{$GradeCrossing}{Signal};
   my($gates) = $$GradeCrossingData{$GradeCrossing}{Gate};

   $main::ChildName = "GcChildProcess$GradeCrossing";
   $result = &DisplayMessage("GcChildProcess${GradeCrossing} started.");
   $result = &DisplayDebug(2, "GcChildProcess${GradeCrossing}, using " .
                              "signalNmbr: $signalNmbr   gates: $gates");

   # Start processing loop.
   while (1) {
      push(@buffer, <STDIN>);
#      if ($#buffer >= 0) {
#         for ($x = 0; $x <= $#buffer; $x++) {
#            print "x: $x - '$buffer[$x]' \n";
#         }
#      }

      # ----------
      # Check for a new complete message and process if found.
      if ($buffer[0] =~ m/(start):(apr)/i or $buffer[0] =~ m/(start):(road)/i or 
          $buffer[0] =~ m/(stop)/i or $buffer[0] =~ m/(exit)/i) {
         $cmd = lc $1;
         $snd = lc $2;
         $result = &DisplayDebug(3, "GcChildProcess${GradeCrossing}, cmd: " .
                                    "'$cmd'   snd: '$snd'");
         splice(@buffer, 0, 1);         # Remove processed record.
      }

      # ----------
      # Process new command, if any.
      if ($cmd ne "") {
         if ($cmd eq "start") {
            if ($lampFlash == 0) {
               $lampColor = 'Red';
               $lampFlash = 1;
            }

            # Start/change the signal bell sound.
            if (&SetSignalColor($$GradeCrossingData{$GradeCrossing}{'Sound'}, 
                                $sound{$snd}, $SignalChildPid, $SignalData, '')) {
               $result = &DisplayError("GcChildProcess${GradeCrossing}, " .
                                    "SetSignalColor for bell on returned error.");
            }
         }
         elsif ($cmd eq "stop" and $lampFlash == 1) {
            $lampColor = 'Off';
         }
         elsif ($cmd eq "exit") {
            $result = &DisplayMessage("GcChildProcess${GradeCrossing} " .
                                      "commanded to exit.");
            select (undef, undef, undef, .5);
            exit(0);
         }
         $cmd = "";                     # Remove processed command.
      }

      # ----------
      # Change lamp state.
      if ($lampFlash == 1) {
         if ($lampColor eq 'Off') {
            $lampFlash = 0;
            if (&SetSignalColor($$GradeCrossingData{$GradeCrossing}{'Sound'}, 
                                'Off', $SignalChildPid, $SignalData, '')) {
               $result = &DisplayError("GcChildProcess${GradeCrossing}, " .
                                "SetSignalColor for bell off returned error.");
            }
         }
         elsif ($lampColor eq 'Red') {
            $lampColor = 'Grn';
         }
         else {
            $lampColor = 'Red';
         }

         if (&SetSignalColor($signalNmbr, $lampColor, $SignalChildPid, 
                             $SignalData, '')) {
            $result = &DisplayError("GcChildProcess${GradeCrossing}, " .
                                    "SetSignalColor returned error.");
         }
      }
      select (undef, undef, undef, .8);    # Sets signal flash rate.
   }
   $result = &DisplayMessage("GcChildProcess${GradeCrossing} terminated.");
   exit(0);
}

# =============================================================================
# FUNCTION:  TestGradeCrossing
#
# DESCRIPTION:
#    This routine cycles the specified grade crossing signal ranges.
#
# CALLING SYNTAX:
#    $result = &TestGradeCrossing($Range, \%GradeCrossingData, \%TurnoutData);
#
# ARGUMENTS:
#    $Range               Signal number or range to use.
#    $GradeCrossingData   Pointer to GradeCrossingData hash.
#    $TurnoutData         Pointer to %TurnoutData hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    $main::MainRun
# =============================================================================
sub TestGradeCrossing {

   my($Range, $GradeCrossingData, $TurnoutData) = @_;
   my($result, $gc, @gates, $gate);
   my(@gcList) = split(",", $Range);

   $result = &DisplayDebug(2, "TestGradeCrossing, Entry ...   Range: '$Range'" .
                              "   gcList: @gcList");

   while ($main::MainRun) {

   # Start approach signal.
      foreach $gc (@gcList) {
         $gc = "0${gc}" if (length($gc) == 1);
         if (exists $$GradeCrossingData{$gc}) {
            $result = &DisplayMessage("TestGradeCrossing, start:apr grade " .
                                      "crossing $gc");
            Forks::Super::write_stdin($$GradeCrossingData{$gc}{'Pid'}, 
                                      'start:apr');
            select (undef, undef, undef, .75);    # Time for realistic lamp start.
         }
         else {
            $result = &DisplayError("TestGradeCrossing, invalid grade " .
                                    "crossing: $gc");
            return 1;
         }

   # Lower gates if grade crossing is so equipt.
         @gates = split(",", $$GradeCrossingData{$gc}{'Gate'});
         foreach $gate (@gates) {
            $result = &DisplayDebug(1, "TestGradeCrossing, Close gate: $gate");
            $result = &MoveTurnout('Close', $gate, $TurnoutData);
            if ($result == 1) {
               $result = &DisplayDebug(1, "TestGradeCrossing, gate: $gate " .
                                          "returned error.");
            }
            elsif ($result == 2) {
               $result = &DisplayDebug(1, "TestGradeCrossing, gate: $gate " .
                                          "returned already in position.");
            }
         }
         Forks::Super::pause 2;
      }
      Forks::Super::pause 4;

# Change to 'road' grade crossing sound. 
      foreach $gc (@gcList) {
         $gc = "0${gc}" if (length($gc) == 1);
         $result = &DisplayMessage("TestGradeCrossing, start:road grade " .
                                   "crossing $gc");
         Forks::Super::write_stdin($$GradeCrossingData{$gc}{'Pid'}, 'start:road');
      }
      Forks::Super::pause 4;

# Stop signal.
      foreach $gc (@gcList) {
         $gc = "0${gc}" if (length($gc) == 1);
         $result = &DisplayMessage("TestGradeCrossing,  stop grade crossing $gc");
         @gates = split(",", $$GradeCrossingData{$gc}{'Gate'});
         foreach $gate (@gates) {
            $result = &MoveTurnout('Open', $gate, $TurnoutData);
            if ($result == 1) {
               $result = &DisplayDebug(1, "TestGradeCrossing, gate: $gate " .
                                          "returned error.");
            }
            elsif ($result == 2) {
               $result = &DisplayDebug(1, "TestGradeCrossing, gate: $gate " .
                                          "returned already in position.");
            }
         }

         # If gates for this crossing, wait for gate open to complete before
         # stopping lamp flash.
         if ($#gates >= 0) {
            $result = &DisplayDebug(1, "TestGradeCrossing, waiting for gate " .
                                       "$gates[0] move to complete.");
            while ($$TurnoutData{$gates[0]}{Pid} > 0) {
               select (undef, undef, undef, .5);
            }
         }
         Forks::Super::write_stdin($$GradeCrossingData{$gc}{'Pid'}, 'stop');
         Forks::Super::pause 2;
      }
      Forks::Super::pause 4;
   }
   return 0;
}

return 1;
