# ============================================================================
# FILE: DnB_Sensor.pm                                              10/26/2018
#
# SERVICES:  DnB SENSOR FUNCTIONS
#
# DESCRIPTION:
#    This perl module provides sensor related functions used by the DnB model 
#    railroad control program.
#
# PERL VERSION: 5.24.1
#
# =============================================================================

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

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

@EXPORT = qw(
   I2C_InitSensorDriver
   KeypadChildProcess
   ButtonChildProcess
   GetSensorBit
   ReadKeypad
   GetButton
   TestSensors
   TestKeypad
);

use DnB_Message;
use Time::HiRes qw(gettimeofday);

# =============================================================================
# FUNCTION:  I2C_InitSensorDriver
#
# DESCRIPTION:
#    This routine initializes the sensor I2C driver board on the DnB model 
#    railroad. It sets parameters that are common to all sensor ports. The 
#    I/O PI Plus board utilizes two MCP23017 chips for a total of 32 I/O 
#    pins. All pins of chips 1, 2, and 4 are configured as inputs.
#
#    Chip 3 is initialized for a 'Storm K Range' 4x4 keypad. MCP23017 GPIO
#    pins are direct connected as follows. Row (letter) GPIOs are set as
#    input + pullup. Columns set as outputs.
#
#    The %SensorChip{chip}{'Obj'} hash key is written with the driver object 
#    pointer for use in sensor data reading.
#
#       Keypad pin:      1 2 3 4 5 6 7 8
#       Keypad col/row:  A B 1 2 3 4 D C
#       GPIOA pin:       3 4 5 6 7 8 9 10
#       GPIOA bit:       0 1 2 3 4 5 6 7
#       GPIODIRA:        1 1 0 0 0 0 1 1     1 = Input, 0 = Output
#
# CALLING SYNTAX:
#    $result = &I2C_InitSensorDriver($ChipNmbr, \%MCP23017, \%SensorChip);
#
# ARGUMENTS:
#    $ChipNmbr             Chip number being initialized.
#    $MCP23017             Pointer to %MCP23017 internal register definitions
#    $SensorChip           Pointer to %SensorChip hash. 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub I2C_InitSensorDriver {

   my($ChipNmbr, $MCP23017, $SensorChip) = @_;
   my($result, $driver);
   my(@temp) = ();

   $result = &DisplayDebug(2, "I2C_InitSensorDriver, ChipNmbr: $ChipNmbr   " .
             "I2C_Address: " . sprintf("0x%.2X",$$SensorChip{$ChipNmbr}{'Addr'}));

   $driver = RPi::I2C->new($$SensorChip{$ChipNmbr}{'Addr'});
   unless ($driver->check_device($$SensorChip{$ChipNmbr}{'Addr'})) {
      $result = &DisplayError("I2C_InitSensorDriver, Failed to initialize I2C " .
                 "address: " . sprintf("0x%.2X",$$SensorChip{$ChipNmbr}{'Addr'}) .
                 " - $!");
      return 1;
   }
   $$SensorChip{$ChipNmbr}{'Obj'} = $driver;

   # Set MCP23017 BANK (bit7) = 0 (sets MCP23017 register addresses)
   $driver->write_byte(0x00, $$MCP23017{'IOCON'});

   # Set all PortA bits to normal bit polarity
   $driver->write_byte(0x00, $$MCP23017{'IOPOLA'}); 

   # Set all PortB bits to normal bit polarity
   $driver->write_byte(0x00, $$MCP23017{'IOPOLB'}); 

   if ($ChipNmbr == 3) {   # 4x4 keypad
      $driver->write_byte(0xC3, $$MCP23017{'IODIRA'});  # Input 7,6,1,0 
      $driver->write_byte(0xC3, $$MCP23017{'GPPUA'});   # Pullup bits 7,6,1,0. 
      $driver->write_byte(0xFF, $$MCP23017{'IODIRB'});  # All PortB bits to input 
   }
   else {   # Read only sensor bits.
      $driver->write_byte(0xFF, $$MCP23017{'IODIRA'});  # All PortA bits to input 
      $driver->write_byte(0xFF, $$MCP23017{'GPPUA'});   # Enable pullup all bits. 
      $driver->write_byte(0xFF, $$MCP23017{'IODIRB'});  # All PortB bits to input 
      $driver->write_byte(0xFF, $$MCP23017{'GPPUB'});   # Enable pullup all bits. 
  }

   push(@temp, $driver->read_byte($$MCP23017{'IOCON'}));   # Get current IOCON.
   push(@temp, $driver->read_byte($$MCP23017{'IODIRA'}));  # Get current IODIRA.
   push(@temp, $driver->read_byte($$MCP23017{'IODIRB'}));  # Get current IODIRB.
   push(@temp, $driver->read_byte($$MCP23017{'IOPOLA'}));  # Get current IOPOLA.
   push(@temp, $driver->read_byte($$MCP23017{'IOPOLB'}));  # Get current IOPOLB.

   $result = &DisplayDebug(2, "I2C_InitSensorDriver - Initialized IODIRA " .
                              "IODIRB IOPOLA IOPOLB IOCON: " . 
                              sprintf("0x%0.2X 0x%0.2X 0x%0.2X 0x%0.2X 0x%0.2X", 
                              @temp));

# Debug code.
#   while (1) {
#      $message = "I2C Address: " . sprintf("0x%.2X",$$SensorChip{$ChipNmbr}
#                                           {'Addr'});
#      $message = $message . "   GPIOB: " . sprintf("%0.8b", 
#                                       $driver->read_byte($$MCP23017{'GPIOB'}));
#      $message = $message . "   GPIOA: " . 
#                      sprintf("%0.8b", $driver->read_byte($$MCP23017{'GPIOA'}));
#      $result = &DisplayMessage("I2C_InitSensorDriver - $message");
#      select (undef, undef, undef, 1);
#   }
#   exit(0);

   return 0;
}

# =============================================================================
# FUNCTION:  KeypadChildProcess
#
# DESCRIPTION:
#    This routine is launched as a child process during main program startup
#    and is used to return user input from the 'Storm K Range' 4x4 button 
#    keypad. This keypad is connected to a MCP23017 port as follows.
#
#       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--
#                 |   |   |   |
#
#       Keypad pin:      1 2 3 4 5 6 7 8
#       Keypad col/row:  A B 1 2 3 4 D C
#       GPIOA pin:       3 4 5 6 7 8 9 10
#       GPIOA bit:       0 1 2 3 4 5 6 7
#       GPIODIRA:        1 1 0 0 0 0 1 1     1 = Input, 0 = Output
#  
#    A dedicated child process is used to improve the reliability of keypad 
#    entries. Forks::Super is used between the parent and child to read data 
#    from the child's STDERR filehandle. Do no other output to STDERR within 
#    this routine. DisplayMessage and DisplayDebug are permitted since they 
#    use STDOUT for messaging.
#
#    The %KeypadData hash provides keypad specific data and state information.
#    Data is accessed using a hash index specified in the $Keypad variable.
#
#    The %cols hash holds 4 values, each has one of the keypad column driver
#    bits low. These bits are configured as outputs by I2C_InitSensorDriver. 
#    The %col hash keys map to the %matrix hash primary keys.
#
#    The %matrix hash contains the resulting button value for each of the 16 
#    combinations of col/row. I2C_InitSensorDriver configures the input pins 
#    with pullup enabled which results in a value of 0xC3 when no button is 
#    pressed. The hash secondary key corresponds to 0xC3 with one of the input 
#    bits low. Input is ignored if multiple buttons are pressed. 
#
# CALLING SYNTAX:
#    $KeypadChildPid = fork {os_priority => 4, sub => \&KeypadChildProcess,
#                            child_fh => "err socket", 
#                            args => [ $Keypad, \%KeypadData, \%MCP23017, 
#                                      \%SensorChip ] };
#
#    $read_key = Forks::Super::read_stderr($KeypadChildPid);
#
# ARGUMENTS:
#    $Keypad                KeypadData entry to use.
#    $KeypadData            Pointer to %KeypadData hash. 
#    $MCP23017              Pointer to MCP23017 internal register definitions
#    $SensorChip            Pointer to %SensorChip hash. 
#
# RETURNED VALUES:
#    0-F = Pressed button via read_stderr.
#
# ACCESSED GLOBAL VARIABLES:
#    $main::ChildName
# =============================================================================
sub KeypadChildProcess {
   my($Keypad, $KeypadData, $MCP23017, $SensorChip) = @_;
   my($result, $col, $row, $button);
   my($chip) = $$KeypadData{$Keypad}{'Chip'};
   my(%cols) = (1 => 0xDF, 2 => 0xEF, 3 => 0xF7, 4 => 0xFB);
   my(%matrix) = (1 => { 0xC2 => '3', 0xC1 => '7', 0x43 => 'B', 0x83 => 'F'},
                  2 => { 0xC2 => '2', 0xC1 => '6', 0x43 => 'A', 0x83 => 'E'},
                  3 => { 0xC2 => '1', 0xC1 => '5', 0x43 => '9', 0x83 => 'D'},
                  4 => { 0xC2 => '0', 0xC1 => '4', 0x43 => '8', 0x83 => 'C'});

   $main::ChildName = 'KeypadChildProcess';
   $result = &DisplayMessage("KeypadChildProcess started.");
   $result = &DisplayDebug(2, "Keypad: $Keypad   chip: $chip");

   if ($$SensorChip{$chip}{'Obj'} == 0) {
      $result = &DisplayMessage("*** error: KeypadChildProcess, No SensorChip " .
                "object for chip $chip. Call I2C_InitSensorDriver routine " .
                "first.");
      $result = &DisplayMessage("KeypadChildProcess terminated.");
      sleep 2;
      exit(0);
   }

   while(1) {
      $button = -1;
      foreach $col (1,2,3,4) {
         $$SensorChip{$chip}{'Obj'}->write_byte($cols{$col}, 
                                     $$MCP23017{ $$KeypadData{$Keypad}{'Col'} });
         select (undef, undef, undef, .02);    # Delay for button debounce.
         $row = $$SensorChip{$chip}{'Obj'}->read_byte($$MCP23017{ 
                                         $$KeypadData{$Keypad}{'Row'} }) & 0xC3;
         $result = &DisplayDebug(3, "ReadKeypad, Keypad: $Keypad   " .
                                 "col: " . sprintf("%0.8b", $cols{$col}) ."   " . 
                                 "row: " . sprintf("%0.8b", $row));

         # Process if valid single button keypress. Ignore held down button.
         if ($row == 0xC2 or $row == 0xC1 or $row == 0x43 or $row == 0x83) {
            $button = $matrix{$col}{$row};          # Get keypress result value.
            if ($button != $$KeypadData{$Keypad}{'Last'}) {   
               $$KeypadData{$Keypad}{'Last'} = $button;
               print STDERR "$button";              # Send key press.
               $result = &DisplayDebug(3, "ReadKeypad, button '$button' " .
                                          "pressed.");
            }
            last;
         }
      }

      # Clear 'Last' if no button is pressed.
      if ($button == -1 and $$KeypadData{$Keypad}{'Last'} != -1) {
         $$KeypadData{$Keypad}{'Last'} = -1;
         select (undef, undef, undef, .02);   # Delay for button debounce.
      }
      select (undef, undef, undef, .1);       # Loop delay.
   }

   $result = &DisplayMessage("KeypadChildProcess terminated.");
   sleep 2;
   exit(0);
}

# =============================================================================
# FUNCTION:  ButtonChildProcess
#
# DESCRIPTION:
#    This routine is launched as a child process during main program startup
#    and is used to return user input from the 'Storm K Range' 1x4 button 
#    keypad. This keypad is connected to a MCP23017 port as follows.
#
#       button    D   C   B   A
#                 |   |   |   |
#       common  --o---o---o---o
#
#         Button Id:       c D C B A
#         Button pin:      1 2 3 4 5
#         GPIOA pin:       2 6 5 4 3
#         GPIOA bit:         4 3 2 1
#
#    A dedicated child process is used to improve the reliability of a double 
#    button press. Forks::Super is used between the parent and child to read 
#    data from the child's STDERR filehandle. Do no other output to STDERR 
#    within this routine. DisplayMessage and DisplayDebug are permitted since 
#    they use STDOUT for messaging.
#
#    Two button data messgaes are generated, single press (s<num>) and double 
#    press (d<num>). <num> is the button index in the %ButtonData hash. The 
#    parent must read the child's data at a rate greater than the expected 
#    user input rate.
# 
#    Multiple button press events may be present in a message, e.g. 's01d01'.
#    Check first for d01 input and discard the s01 input if present.
#
#    The %ButtonData{<num>}{'Obj'} references must be set prior to launching
#    this child process.
#
# CALLING SYNTAX:
#    $ButtonChildPid = fork {os_priority => 4, sub => \&ButtonChildProcess,
#                            child_fh => "err socket", 
#                            args => [ \%ButtonData, \%MCP23017, \%SensorChip ] };
#
#    $read_button = Forks::Super::read_stderr($ButtonChildPid);
#
# ARGUMENTS:
#    $ButtonData            Pointer to %ButtonData hash.
#    $MCP23017              Pointer to MCP23017 internal register definitions
#    $SensorChip            Pointer to %SensorChip hash. 
#
# RETURNED VALUES:
#    s<num> - Button <num> has been single pressed. 
#    d<num> - Button <num> has been double pressed. 
#
# ACCESSED GLOBAL VARIABLES:
#    $main::ChildName
# =============================================================================
sub ButtonChildProcess {
   my($ButtonData, $MCP23017, $SensorChip) = @_;
   my($result, $button, $port, $mask, $chip, $check);

   $main::ChildName = 'ButtonChildProcess';
   $result = &DisplayMessage("ButtonChildProcess started.");

   while(1) {
      foreach $button (keys %$ButtonData) {
         $chip = $$ButtonData{$button}{'Chip'};
         if ($$ButtonData{$button}{'Bit'} =~ m/^(GPIO.)(\d)/) {
            $port = $1;
            $mask = 1 << $2;

            # Read the port, invert the value (could also be done by MCP23017 
            # using IPOL register), and isolate the bit value.  
            $check = $$SensorChip{$chip}{'Obj'}->read_byte($$MCP23017{$port});
            $check = (~$check) & $mask;

            # 'Last' is used to handle a held down button. Only use the
            # transition from 0 to 1 as a button press.
            if ($check != 0) {
               if ($$ButtonData{$button}{'Last'} == 1) {
                  $$ButtonData{$button}{'PressTime'} = gettimeofday;
                  next;
               }
               if ((gettimeofday - $$ButtonData{$button}{'PressTime'}) < 1) {
                  
                  print STDERR "d${button}";               # Send double press.
                  $$ButtonData{$button}{'PressTime'} = 0;  # New press cycle.
                  $result = &DisplayDebug(3, "ButtonChildProcess, button: " .
                                             "d${button}");
               }
               else {
                  print STDERR "s${button}";               # Send single press
                  $$ButtonData{$button}{'PressTime'} = gettimeofday;
                  $result = &DisplayDebug(3, "ButtonChildProcess, button: " .
                                             "s${button}");
               }
               $$ButtonData{$button}{'Last'} = 1;
            }
            else {
               $$ButtonData{$button}{'Last'} = 0;          # Button released.
            }
         }
         select (undef, undef, undef, .05);   # Loop delay.
      }
   }

   $result = &DisplayMessage("ButtonChildProcess terminated.");
   sleep 2;
   exit(0);
}

# =============================================================================
# FUNCTION:  GetSensorBit
#
# DESCRIPTION:
#    This routine returns the current value of the specified sensor bit.
#
# CALLING SYNTAX:
#    $result = &GetSensorBit($BitNumber, \%SensorBit, \%SensorState);
#
# ARGUMENTS:
#    $BitNumber             Bit position to check (index in %SensorBit)
#    $SensorBit             Pointer to %SensorBit hash.
#    $SensorState           Pointer to %SensorState hash. 
#
# RETURNED VALUES:
#    Bit value: 0 or 1
#
# ACCESSED GLOBAL VARIABLES:
#    None.
# =============================================================================
sub GetSensorBit {
   my($BitNumber, $SensorBit, $SensorState) = @_;
   my($result);
   my($bitMask) = 1 << ($BitNumber % 16);

   return 1 if ($$SensorState{ $$SensorBit{$BitNumber}{'Chip'} } & $bitMask);

   return 0;
}

# =============================================================================
# FUNCTION:  TestSensors
#
# DESCRIPTION:
#    This routine displays the sensor state bits on the console. The user can
#    manually activate each sensor and observe the expected result. This test
#    loops indefinitely until the user enters ctrl+c.
#
# CALLING SYNTAX:
#    $result = &TestSensors($Range, \%MCP23017, \%SensorChip, \%SensorState);
#
# ARGUMENTS:
#    $Range                    Chip number or range to use.
#    $MCP23017                 Pointer to MCP23017 internal register definitions
#    $SensorChip               Pointer to %SensorChip hash. 
#    $SensorState              Pointer to %SensorState hash.
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    $main::MainRun
# =============================================================================
sub TestSensors {
   my($Range, $MCP23017, $SensorChip, $SensorState) = @_;
   my($result, $chip, $start, $end, $msg, @chipList);

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

   if ($Range =~ m/(\d+):(\d+)/) {   # Range specified.
      $start = $1;
      $end = $2;
      if ($start > $end or $start < 1 or $start > 4 or $end < 1 or $end > 4) {
         $result = &DisplayError("TestSensors, invalid sensor chip range: " .
                                 "'$Range'");
         return 1;
      }
      for ($chip = $start; $chip <= $end; $chip++) {
         push (@chipList, $chip);
      }
   }
   else {
      @chipList = split(",", $Range);
   }
   $result = &DisplayDebug(1, "TestSensors, chipList: '@chipList'");

   $result = &DisplayMessage("TestSensors - enter ctrl+c to exit.\n");
   $result = &DisplayMessage("TestSensors -----                    bit 76543210" .
                             "      bit 76543210");
   while($main::MainRun) {
      foreach $chip (@chipList) {
         if (exists($$SensorChip{$chip})) {
            $$SensorState{$chip} = 
               ($$SensorChip{$chip}{'Obj'}->read_byte($$MCP23017{'GPIOB'}) << 8) |
                $$SensorChip{$chip}{'Obj'}->read_byte($$MCP23017{'GPIOA'});

            $msg = "I2C Address: " . sprintf("0x%.2X", $$SensorChip{$chip}
                                             {'Addr'});
            $msg = $msg . "   GPIOB: " . 
                          sprintf("%0.8b", ($$SensorState{$chip} >> 8));
            $msg = $msg . "   GPIOA: " . 
                          sprintf("%0.8b", ($$SensorState{$chip} & 0xFF));
            $result = &DisplayMessage("TestSensors - $msg");
         }
         else {
            $result = &DisplayError("TestSensors, invalid sensor range: " .
                                    "'$Range'");
            return 1;
         }
      }
      select (undef, undef, undef, 1);
   }
   return 0;
}

# =============================================================================
# FUNCTION:  TestKeypad
#
# DESCRIPTION:
#    This routine displays the pressed button on the keypad. It also sets and 
#    resets the 1st entry LED with each key press. The individual turnout 
#    buttons will also be displayed when pressed. This test loops until the 
#    user enters ctrl+c.
#
# CALLING SYNTAX:
#    $result = &TestKeypad($Keypad, \%KeypadData, \%ButtonData, \%GpioData, 
#                          \%MCP23017, \%SensorChip, \%ButtonChild);
#
# ARGUMENTS:
#    $KeypadId              KeypadData entry to test.
#    $KeypadData            Pointer to %KeypadData hash.
#    $ButtonData            Pointer to %ButtonData hash.
#    $GpioData              Pointer to %GpioData hash.
#    $MCP23017              Pointer to MCP23017 internal register definitions
#    $SensorChip            Pointer to %SensorChip hash. 
#    $KeypadChildPid        $KeypadChildPid value.
#    $ButtonChildPid        $ButtonChildPid value. 
#
# RETURNED VALUES:
#    0 = Success,  1 = Error.
#
# ACCESSED GLOBAL VARIABLES:
#    $main::MainRun
# =============================================================================
sub TestKeypad {
   my($KeypadId, $KeypadData, $ButtonData, $GpioData, $MCP23017, $SensorChip,
      $KeypadChildPid, $ButtonChildPid) = @_;
   my($result, $button, $value, $key, @input);

   $result = &DisplayDebug(2, "TestKeypad, Entry ... KeypadId: $KeypadId");

   $KeypadId = "0$KeypadId" if (length($KeypadId) == 1);
   if (exists $$KeypadData{$KeypadId}) {
      while($main::MainRun) {

         # Keypad buttons.
         $button = Forks::Super::read_stderr($KeypadChildPid);
         if ($button eq '') {
            $result = &DisplayMessage("TestKeypad, KeypadId: $KeypadId - No " .
                                      "button pressed.");
         }
         else {
            $button = substr($button, 0, 1);  # 1st character only if multiple.
            $result = &DisplayMessage("TestKeypad, KeypadId: $KeypadId - " .
                                      "keypad button pressed: '$button'");

            # Read keypad 1st entry LED.
            $value = $$GpioData{ $$KeypadData{$KeypadId}{'Gpio'} }{'Obj'}->read;

            # Compliment the value.
            $value = (~$value) & 1;

            # Write keypad 1st entry LED.
            $$GpioData{ $$KeypadData{$KeypadId}{'Gpio'} }{'Obj'}->write($value);
            $result = &DisplayMessage("TestKeypad, KeypadId: $KeypadId - 1st " .
                                      "entry LED set to $value");
         }

         # Single buttons.
         $button = Forks::Super::read_stderr($ButtonChildPid);
         if ($button ne '') {
            if ($button =~ m/d(\d+)/) {
               $result = &DisplayMessage("TestKeypad, button double press: " .
                                         "$$ButtonData{$1}{'Desc'}");
            }
            elsif ($button =~ m/s(\d+)/) {
               $result = &DisplayMessage("TestKeypad, button single press: " .
                                         "$$ButtonData{$1}{'Desc'}");
            }
            else {
               $result = &DisplayMessage("TestKeypad, invalid button response: " .
                                         "'$button'");
            }
         }
         else {
            $result = &DisplayMessage("TestKeypad, No single button input.");
         }
         select (undef, undef, undef, 2);
      }
   }
   else {
      $result = &DisplayError("TestKeypad, Keypad $KeypadId is not supported.");
      return 1;
   }

   return 0;
}

return 1;
