=begin
 ******************************************************************************
 * Copyright 1990-2009 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Interface to the relay board
 * Filename : relay_board.rb
 * Author   : A. Chase, Henthorn
 * Project  : Benthic Rover
 * Version  : 3
 * Created  : May-06  Basic functionality (relay-on/off)
 * Modified : May-07  RelayMan class, higher-level functions, handles multiple
 *                    boards and dependencies between relays
 *            Nov-08  Barbo integration
 ******************************************************************************
=end

# Refer to Rover-code home directory as base
#
require "#{ENV['ROVER_HOME']}/utils/rover_environment"
#
require 'monitor'
require 'rubygems'
require 'serialport'
require 'utils/datalog'
require 'utils/misc'

##########
# Interface to serial relay board controller. Used by RelayMan class
# to control Rover relays.
#
class RelayBoard
  include SyslogWriter

  attr_reader :serial_port

  ##
  # Initializer. Does not assume power is on.
  #
  def initialize config, portname='/dev/relay'
    @portname = portname
    unless config
      config = SerialPort::Configuration.default
      config.baud = 9600
    end
    @config = config
    @serial_port = Posix::SerialPort.new portname, config
    @lock = Monitor.new
  end
  #####

  ##
  # Init method called after power is on
  #
  def init
  end
  #####


  ##
  # Is this a valid relay number?
  #
  def valid_relay?(relay_num)
    if (relay_num && (relay_num >= 0) && (relay_num <= 8))
      true
    else
      syslog("! invalid relay number: #{relay_num}")
      false
    end
  end
  #####

  ##
  # Chew past the prompt. Prompt consists of 3 characters
  #
  def flush_prompt
    @serial_port.read(3)
  end
  #####

  ##
  # Send a command to the board to close a given relay. Returns nil
  # if successful. Parameter relay_num expected to be an integer.
  # Logs invalid relay number
  # 
  def turn_on(relay_num)
  @lock.synchronize do
    return if (!valid_relay?(relay_num))

    # Send command to device, read back echo, flush prompt
    num_bytes = write("N#{relay_num}\r")
    read = @serial_port.read(num_bytes - 1)
    flush_prompt
    nil
  end
  end
  #####

  ##
  # Send a command to open a given relay. Returns nil on success.
  # if successful. Parameter relay_num expected to be an integer.
  # Logs invalid relay number
  #
  def turn_off(relay_num)
  @lock.synchronize do
    return if (!valid_relay?(relay_num))

    # Send command to device, read back echo, flush prompt
    num_bytes = write("F#{relay_num}\r")
    read = @serial_port.read(num_bytes - 1)
    flush_prompt
    nil
  end
  end
  #####

  def write(str)
    nb = 0
    begin
      nb = @serial_port.write(str)
    rescue Errno::ESPIPE
      syslog("! Caught ESPIPE exception. Attempt to write again.")
      reset_port()
      retry
      nb = @serial_port.write(str)
    end
    return nb
  end

  def reset_port
    @serial_port.close
    sleep(0.1)
    @serial_port = Posix::SerialPort.new(@portname, @config)
  end

  ##
  # Return status of relay (open or closed). 0 = open
  #
  def get_status(relay_num)
  @lock.synchronize do
    return if (!valid_relay?(relay_num))

    # Send command to device, read back echo, read relay status, flush prompt
    num_bytes = write("S#{relay_num}\r")
    line = @serial_port.read(num_bytes + 1)
    if(relay_num == 0)
      line = @serial_port.read(2)
    else
      line = @serial_port.read(1)
    end
    flush_prompt
    line 
  end
  end
  #####

end

##########
# High-level interface to Rover relays, a power manager of sorts.
#
class RelayMan
  include SyslogWriter

  # Define relay switches. Definitions match those on the wiring diagram.
  #
  ######### Rover Relay board 1 ##########
  MOTORS     = 1
  V12A       = 2  # 12V for video and cleaning pumps
  V12B       = 3  # 12V for cameras, valve pumps, optodes, acm
  V5         = 4  # For stir motors
  S_VALVE    = 5  # Starboard valve pump, depends on V12B
  OPTODES    = 6  # Chamber optodes, depends on V12B
  S_RACK     = 7  # Starboard rack motor
  STIR       = 8  # Stir motors, depends on 5V (phasing-out P_STIR & S_STIR)

  #
  ######### Rover Relay board 2 ##########
  P_RACK    = 1  # Port rack motor
  CAMERAS   = 2  # Both cameras, depends on V12B
  TRANSCAM  = 3  # Transit Prosilica camera, depends on V12B
  P_VALVE   = 4  # Port valve pump, depends on V12B
  FLUOROCAM = 5  # Fluorometry camera
  P_REFOPT  = 6  # Port reference optode V5
  S_REFOPT  = 7  # Stbd reference optode V5
  VIDEO     = 8  # Depends on V12A

  attr_reader :rb1, :rb2, :acmIsOn

  ##
  # Initialize two relay boards, all relays open
  #
  def initialize
    @acmIsOn = false
    @rb1 = RelayBoard.new(nil, "/dev/relay1")
    @rb2 = RelayBoard.new(nil, "/dev/relay2")
    
    self.allOff
    syslog("_ RelayMan initialized")
  end
  #####

  NoComms    = "no comms"
  BadRelay1  = "bad board1"
  BadRelay2  = "bad board2"

  #####
  def check_up
    allOff
    ret = nil

    t = RoverThread.new do
      ##
      # Assuming all devices are off, get the status of the first
      # relay on each board. Should be 0.
      #
      stat = rb1.get_status(1)
      if (stat.to_i != 0)
        ret = BadRelay1
      else

        stat = rb2.get_status(1)
        if (stat.to_i != 0)
          ret = BadRelay2
        end
      end
    end
    sleep(5)

    if (t.alive?)
      t.kill
      t.join
      ret = NoComms
    end
    
    ret
  end
  #####
  
  ##
  # 5-volt power on
  def v5On
    @rb1.turn_on(V5)
    syslog("_ 5V On")
  end
  #####

  ##
  # 5-volt power off
  def v5Off
    @rb1.turn_off(V5)
    syslog("_ 5V Off")
  end
  #####

  ##
  # 12-volt power, set A, on
  def v12AOn
    @rb1.turn_on(V12A)
    syslog("_ 12V-A on")
  end
  #####

  ##
  # 12-volt power, set A, off
  def v12AOff
    @rb1.turn_off(V12A)
    syslog("_ 12V-A off")
  end
  #####

  ##
  # 12-volt power, set B, on
  def v12BOn
    @rb1.turn_on(V12B)
    syslog("_ 12V-B on")
  end
  #####

  ##
  # 12-volt power, set B, off
  def v12BOff
    @rb1.turn_off(V12B)
    syslog("_ 12V-B off")
  end
  #####

  ##
  # Return true if no devices that need the 12vA relay on
  # are currently on. Used to determine if Rover can
  # shutdown power to 12V-A.
  #
  def v12ANotNeeded?
    if (@rb2.get_status(VIDEO) == "0")
      true   # everything on V12A is off
    else
      false
    end
  end
  #####

  ##
  # Return true if no devices that need the 12vB relay on
  # are currently on. Used to determine if Rover can also
  # shutdown power to 12V-B.
  # 
  def v12BNotNeeded?
    if (@rb2.get_status(P_VALVE)=="0" && @rb1.get_status(S_VALVE)=="0" && \
        @rb2.get_status(CAMERAS)=="0" && !@acmIsOn)
      true   # everything on V12B is off
    else
      false
    end
  end
  #####

  ##
  # Return true if no devices that need the 5v relay on
  # are currently on. Used to determine if Rover can also
  # shutdown power to 5V.
  ##
  def v5NotNeeded?
    if (@rb1.get_status(STIR) == "0" && @rb1.get_status(OPTODES)=="0" && \
        @rb2.get_status(P_REFOPT) == "0" && @rb2.get_status(S_REFOPT) == "0")
      true   # everything on V5 is off
    else
      false
    end
  end
  #####

  ##
  # Turn on the acm by turning on V12B
  # Acm does not have it's own switch, so we keep track with boolean acmIsOn
  #
  def acmOn
    v12BOn
    @acmIsOn = true  # since acm == v12B
    syslog("_ acm on")
  end
  #####

  ##
  # Flag that power to acm is not needed, may have to keep
  # v12B relay closed because other devices still need it.
  #
  def acmOff
    @acmIsOn = false
    if v12BNotNeeded?
      v12BOff
      syslog("_ acm power off")
    else
      syslog("_ acm not needed")
    end
  end
  #####

  ##
  # Turn on port ref optode first turning on 5V 
  # 
  def p_refoptOn
    v5On
    @rb2.turn_on(P_REFOPT)
    syslog("_ port ref optode on")
  end
  #####

  ##
  # Turn off port ref optode: (1) p_refopt off
  #                           (2) 5V off only if all other 5V devices are off
  # 
  def p_refoptOff
    @rb2.turn_off(P_REFOPT)
    syslog("_ port ref optode off")
    v5Off if v5NotNeeded?
  end
  #####

  ##
  # Turn on stbd ref optode first turning on 5V 
  # 
  def s_refoptOn
    v5On
    @rb2.turn_on(S_REFOPT)
    syslog("_ stbd ref optode on")
  end
  #####

  ##
  # Turn off stbd ref optode: (1) s_refopt off
  #                           (2) 5V off only if all other 5V devices are off
  # 
  def s_refoptOff
    @rb2.turn_off(S_REFOPT)
    syslog("_ stbd ref optode off")
    v5Off if v5NotNeeded?
  end
  #####

  ##
  # Turn on video server by first turning on 12VA and then video
  # 
  def videoOn
    self.camerasOn
    v12AOn
    @rb2.turn_on(VIDEO)
    syslog("_ video on")
  end
  #####

  ##
  # Turn off video: (1) video off
  #                 (2) 12VA off only if all other 12vA devices are off
  # 
  def videoOff
    self.camerasOff
    @rb2.turn_off(VIDEO)
    syslog("_ video off")
    v12AOff if v12ANotNeeded?
  end
  #####

  ##
  # Turn on cameras by first turning on 12VB and then cameras
  # 
  def camerasOn
    v12BOn
    @rb2.turn_on(CAMERAS)
    syslog("_ cameras on")
  end
  #####

  ##
  # Turn off cameras: (1) cameras off
  #                   (2) 12VB off only if all other 12vB devices are off
  # 
  def camerasOff
    @rb2.turn_off(CAMERAS)
    syslog("_ cameras off")
    v12BOff if v12BNotNeeded?
  end
  #####

  ##
  # Turn on starboard rack motor
  # 
  def s_rackOn
    @rb1.turn_on(S_RACK)
    syslog("_ starboard rack on")
  end
  #####

  ##
  # Turn off starboard rack motor
  # 
  def s_rackOff
    @rb1.turn_off(S_RACK)
    syslog("_ starboard rack off")
  end
  #####

  ##
  # Turn on port rack motor
  # 
  def p_rackOn
    @rb2.turn_on(P_RACK)
    syslog("_ port rack on")
  end
  #####

  ##
  # Turn off port rack motor
  # 
  def p_rackOff
    @rb2.turn_off(P_RACK)
    syslog("_ port rack off")
  end
  #####

  ##
  # Turn on the propulsion motors
  # 
  def motorsOn
    @rb1.turn_on(MOTORS)
    syslog("_ propulsion motors on")
  end
  #####

  ##
  # Turn off the propulsion motors
  # 
  def motorsOff
    @rb1.turn_off(MOTORS)
    syslog("_ propulsion motors off")
  end
  #####

  ##
  # Turn on the transit cam.
  #
  def transitOn
    @rb2.turn_on(TRANSCAM)
    syslog("_ transit camera on")
  end
  #####

  ##
  # Turn off the transit cam.
  #
  def transitOff
    @rb2.turn_off(TRANSCAM)
    syslog("_ transit camera off")
  end
  #####

  ##
  # Turn on fluoro cam
  # 
  def fluoroOn
    @rb2.turn_on(FLUOROCAM)
    syslog("_ RelayMan: fluoro cam on")
  end
  #####

  ##
  # Turn off fluoro cam
  # 
  def fluoroOff
    @rb2.turn_off(FLUOROCAM)
    syslog("_ RelayMan: fluoro cam off")
  end
  #####

  ##
  # Turn on the optodes
  # (1) turn on both 12v supplies
  # (2) turn on optodes
  # 
  def optodesOn
    v5On
    @rb1.turn_on(OPTODES)
    syslog("_ optodes on")
  end
  #####

  ##
  # Turn off the optodes
  # (1) turn off optodes
  # (2) turn off 5v if not needed
  # 
  def optodesOff
    @rb1.turn_off(OPTODES)
    syslog("_ optodes off")
    v5Off if v5NotNeeded?
  end
  #####

  ##
  # Turn on starboard valve pump
  #  
  def s_valveOn
    v12BOn
    @rb1.turn_on(S_VALVE)
    syslog("_ starboard valve open")
  end
  #####

  ##
  # Turn off starboard valve pump
  # Turn off power if not needed
  # 
  def s_valveOff
    @rb1.turn_off(S_VALVE)
    syslog("_ starboard valve closed")
    v12BOff if v12BNotNeeded?
  end
  #####

  ##
  # Turn on port valve pump. Depends on V12B.
  #  
  def p_valveOn
    v12BOn
    @rb2.turn_on(P_VALVE)
    syslog("_ port valve open")
  end
  #####

  ##
  # Turn off port valve pump
  # Turn off power if not needed
  # 
  def p_valveOff
    @rb2.turn_off(P_VALVE)
    syslog("_ port valve closed")
    v12BOff if v12BNotNeeded?
  end
  #####

  ##
  # Turn on stir motors. Depends on V5.
  # 
  def stirOn
    v5On
    #sleep(0.05)
    @rb1.turn_on(STIR)
    syslog("_ stir motors on")
  end
  #####

  ##
  # Turn off stir motors. Depends on V5.
  # 
  def stirOff
    @rb1.turn_off(STIR)
    syslog("_ stir motors off")
    v5Off if v5NotNeeded?
  end
  #####

  ##
  # Open all relays
  #
  def allOff
    for i in 1..8
      #@rb1.turn_off(i)
      #@rb2.turn_off(i)
      
      # Keep the video server on while on MARS (V12A + VIDEO)
      @rb1.turn_off(i) unless ((i == V12A)  || (i == V12B))
      @rb2.turn_off(i) unless ((i == VIDEO) || (i == CAMERAS))
      
      # Keep the video server on while on MARS (V12A + VIDEO)
      @rb1.turn_off(i)
      @rb2.turn_off(i)
    end
    syslog("_ RelayMan: all off")
  end
  #####

end
##########


##########
# SimRelayBoard: Simulated RelayBoard class
#
class SimRelayBoard < RelayBoard
  include SyslogWriter

  ##
  # Initialize relay_status hash
  #
  def initialize(config, portname='/dev/relay')
    syslog("_ initializer called with portname = #{portname}")
    @relay_status = {1 => "0", 2 => "0", 3 => "0", 4 => "0",
                     5 => "0", 6 => "0", 7 => "0", 8 => "0"}
  end
  #####

  def get_status(relay_num)
    return if (!valid_relay?(relay_num))
    @relay_status[relay_num]
  end
  #####
  
  def turn_on(relay_num)
    return if (!valid_relay?(relay_num))
    @relay_status[relay_num] = "1"
    nil
  end
  #####
  
  def turn_off(relay_num)
    return if (!valid_relay?(relay_num))
    @relay_status[relay_num] = "0"
    nil
  end  
  #####

end
##########


##########
# SimRelayMan: Simulated RelayMan derives from RelayMan
#
class SimRelayMan < RelayMan
  include SyslogWriter

  def initialize
    @acmIsOn = false
    @rb1 = SimRelayBoard.new nil, "/dev/relay1"
    @rb2 = SimRelayBoard.new nil, "/dev/relay2"
    
    self.allOff
    syslog("_ initialized")
  end

end
##########


##
# Standalone unit test (recommended)
# Include at the end of the file as a hook to run a unit test of
# the code from the command line (e.g., "$ ruby  my_class.rb")
#
if __FILE__ == $0
  # Test code here
  if (ARGV.length > 0)
    rm = SimRelayMan.new
  else
    rm = RelayMan.new
  end

  # Use cameras to test acm flag thing since they both rely on V12B
  rm.acmOn
  sleep(0.1)
  rm.camerasOn
  sleep(0.1)
  rm.acmOff
  sleep(0.1)
  rm.camerasOff
  rm.acmOff

  rm.s_cleanOn
  sleep(0.1)
  rm.s_cleanOff

  rm.p_cleanOn
  sleep(0.1)
  rm.p_cleanOff

  rm.videoOn
  sleep(0.1)
  rm.videoOff

  rm.camerasOn
  sleep(0.1)
  rm.camerasOff

  rm.s_rackOn
  sleep(0.1)
  rm.s_rackOff

  rm.p_rackOn
  sleep(0.1)
  rm.p_rackOff

  rm.motorsOn
  sleep(0.1)
  rm.motorsOff

  rm.transitOn
  sleep(0.1)
  rm.transitOff

  rm.optodesOn
  sleep(0.1)
  rm.optodesOff

  rm.s_valveOn
  sleep(0.1)
  rm.s_valveOff

  rm.p_valveOn
  sleep(0.1)
  rm.p_valveOff

  rm.stirOn
  sleep(0.1)
  rm.stirOff

  rm.allOff
end
