#!/usr/local/bin/ruby
#
=begin
 ******************************************************************************
 * Copyright 1990-2011 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Wakey board driver. Power control and hibernate controller.
 * Filename : wakey.rb
 * Author   : Henthorn
 * Project  : SES/Benthic Rover
 * Version  : 1
 * Created  : August 2011
 * Modified : June 2014   RGH  Integration with Rover 
 ******************************************************************************
=end

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

require 'posix/serialport'
require 'utils/datalog'
require 'utils/rover_thread'

##########
# Implements a driver/logger function for the SES wakey board.
# "setdelay" "sleep" "setsleep" "set12von" "set12voff" "readwake" "read12v"
#

class Wakey 
  include SyslogWriter

  attr_reader :dev, :name, :wake_status, :sn
  attr_reader :sleep_time

  # Args: serial port name.
  # Typical usage: op = Wakey.new('/dev/port')
  #
  def initialize(p_portname = '/dev/ttyAM0')
    serial_config_obj = SerialPort::Configuration.default
    serial_config_obj.baud = 9600

    @dev = Posix::SerialPort.new(p_portname, serial_config_obj)
    `stty -F #{p_portname} -opost -isig -icanon -echo`

    @name = File.basename(p_portname)
    @model = "wakey board 1.0"
    @sn = "1"

    log_filename = DataLog.system_logs + "/voltage.csv"
    @log = WakeyLog.new(log_filename)

    # Initialize the object variables and constants
    @sleep_time = 1

    # FIXME: read sometimes returns nil
    #
    @sn = self.read_serial_num.to_i
    self.read_wake()
    #self.set_delay_time(5)  #seconds
    
    self.log_voltage()
  end

  NoComms = "no comms"
  BadData = "bad data"
  NormalReset  = 0       # "normal reset" : power reapplied to the wakey board
  NormalWake   = 1       # "normal wakeup": power reapplied after hibernation
  ModemTraffic = 2       # "": wakey alerted by modem traffic
  OtherWake    = 3       # Did not read a normal response to 'readwake'
  

  #####
  # Query the serial number and return the respones
  #
  def read_serial_num
    @dev.write("readserno\r")
    get_msg
  end
  
  #####
  # Log the battery voltage
  #
  def log_voltage
    v = "error" unless (v = self.read_batt())
    @log.write(v)
  end
  #####
  
  
  #####
  # Turn 12V power on
  #
  def v12_on
    #self.log_voltage()
    @dev.write("set12von\r")
    get_msg
  end
  #####


  #####
  # Turn 12V power off
  #
  def v12_off
    @dev.write("set12voff\r")
    get_msg
  end
  #####


  #####
  # Hibernate the given amount of minutes
  # Caller should see the minutes returned
  # 
  def hibernate_setup(minutes)
    minutes = minutes.to_i
    syslog("_ hibernate_setup(#{minutes})")
    return nil if (minutes < 1)

    # Set the delay time to 2 seconds before shutdown
    #
    syslog("_ hibernate_setup delay 2 seconds")
    response = set_delay_time(2)
    return nil unless (response.to_i == 2)

    # Set the hibernation duration in minutes
    #
    syslog("_ hibernate_setup hibernate for #{minutes} minutes")
    response = set_hibernate_time(minutes)
    return response unless (response.to_i == minutes)

    return response.to_i
  end
  #####



  #####
  # Hibernate the set amount of minutes
  # Returns the sleep duration in minutes if successful
  #
  def hibernate(minutes=nil)
    msg = hibernate_setup(minutes) if (minutes)
    return msg unless (msg.to_i == minutes.to_i)

    self.log_voltage()

    # Tell wakey to sleep, check response
    #
    syslog("_ hibernate says \"sleep\"")
    @dev.write("sleep\r")
    msg = get_msg(0.5)

    return nil unless (msg)

    tokens = msg.split
    return nil unless (tokens[1] == "Sleeping")

    #syslog("_ umount /mnt/usb0")
    #puts(`umount /mnt/usb0`)
    
    return tokens[2].to_i
  end
  #####


  #####
  # Read the maximum time wakey will allow the system to remain awake
  # "readtimeout"
  #
  def read_timeout
    @dev.write("readtimeout\r")
    msg = get_msg
    if (msg)
      syslog("_ read_timeout? #{msg}")
      tokens = msg.split
      msg = tokens[4] if (tokens.length == 7)
    else
      syslog("! no response to readtimeout")
    end
    msg
  end
  #####


  #####
  # Set the maximum time wakey will allow the system to remain awake
  # "settimeout"
  #
  def set_timeout(minutes)
    syslog("_ Setting timeout time to #{minutes} minutes")
    return nil unless (minutes.to_i >= 0 && minutes.to_i <= 180)
    
    @dev.write("settimeout\r")
    msg = get_msg
    tokens = msg.split if (msg)
    unless (msg && tokens.length == 11 && tokens[7] == "1092,")
      if (msg)
        syslog ("! Bad settimeout response: #{tokens}") if (msg)
      else
        syslog ("! No settimeout response")
      end
      return nil
    end
    
    val = (minutes.to_i).to_s + "\r"
    syslog ("_ No settimeout response") unless (msg)
    @dev.write(val)
    if (msg = get_msg(1))
    else
      return nil
    end
    minutes
  end
  #####


  #####
  # Inquire about the hibernate time
  #
  def read_hibernate
    @dev.write("readsleep\r")
    msg = get_msg
    if (msg)
      syslog("_ read_hibernate? #{msg}")
      tokens = msg.split
      msg = tokens[4] if (tokens.length == 7)
    else
      syslog("! no response to readsleep")
    end
    msg
  end
  #####


  #####
  # Inquire about the battery voltage
  #
  def read_batt
    @dev.write("readbatt\r")
    msg = get_msg(0.5)
    if (msg)
      syslog("_ readbatt? #{msg}")
      tokens = msg.split
      msg = tokens[1] if (tokens.length == 3)
    else
      syslog("! no response to readbatt")
    end
    msg
  end
  #####


  #####
  # Inquire what caused the latest power-up
  #
  def read_wake
    @dev.write("readwake\r")
    msg = get_msg
    if (msg)
      syslog("_ readwake? #{msg}")
      
      ##
      # Attempt to determine cause of last power startup
      #
      if (msg.include?("normal wakeup"))
        @wake_status = NormalWake
      elsif (msg.include?("normal reset"))
        @wake_status = NormalReset
      else
        @wake_status = OtherWake
      end

      tokens = msg.split
      msg = tokens[1] if (tokens.length == 3)
    else
      syslog("! no response to readwake")
    end
    msg
  end
  #####


  #####
  # Inquire about 12V power
  #
  def v12?
    @dev.write("read12v\r")
    msg = get_msg
    if (msg)
      syslog("_ v12? #{msg}")
      tokens = msg.split
      if (tokens.length == 5)
        msg = tokens[3]
        if (msg == "ON")
          return true
        elsif (msg == "OFF")
          return false
        end
      end
    else
      syslog("! no response to readwake")
    end
    msg
  end
  #####


  #####
  # Set the delay time
  #
  def set_delay_time(seconds)
    syslog("_ Setting delay time to #{seconds} seconds")
    return nil unless (seconds.to_i > 0 && seconds.to_i <= 60)
    
    @dev.write("setdelay\r")
    msg = get_msg
    tokens = msg.split if (msg)
    unless (msg && tokens.length == 9 && tokens[8] == "60):")
      syslog ("! Bad setdelay response: #{tokens}") if (msg)
      syslog ("! No setdelay response") unless (msg)
      return nil
    end
    
    val = (seconds.to_i).to_s + "\r"
    @dev.write(val)
    if (msg = get_msg(0.5))
      syslog("_ #{msg}")
      msg = msg.split
      syslog("_ #{msg[4]}")
    else
      return nil
    end
    seconds
  end
  #####


  #####
  # Set the hibernate time
  #
  def set_hibernate_time(minutes)
    syslog("_ Setting hibernation time to #{minutes} minutes")
    return nil unless (minutes.to_i > 0 && minutes.to_i <= 1092)
    
    @dev.write("setsleep\r")
    msg = get_msg
    tokens = msg.split if (msg)
    unless (msg && tokens.length == 9 && tokens[8] == "1092):")
      syslog ("! Bad setsleep response: #{tokens}") if (msg)
      syslog ("! No setsleep response") unless (msg)
      return nil
    end
    
    val = (minutes.to_i).to_s + "\r"
    @dev.write(val)
    if (msg = get_msg(0.5))
    else
      return nil
    end
    minutes
  end
  #####


  #####
  # Perform a basic health check-up by attempting to acquire data.
  #
  def check_up
    ##
    # Execute in a separate thread to catch comms errors.
    # Timeout after 15 seconds.
    #
    got_data = nil

    t = RoverThread.new do
      @dev.write("read12v\r")
      sleep(1)
      got_data = get_msg
    end
    sleep(5)

    ##
    # If check-up thread still running, kill it and
    # report NoComms
    #
    if (t.alive?)
      t.kill
      t.join
      syslog("_ no comms")
      return NoComms
    end
    t.join

    if (got_data == nil)
      syslog("_ bad data")
      return BadData
    else
      syslog("_ OK")
      return nil  # Passed
    end
  end 


  #####
  # Read results of a command
  #
  def get_response
    msg = get_msg

    ##
    # Return nil if there is no message
    unless (msg)
      syslog("! No response read")
      return nil
    end

    msg
  end
  #####


  ##
  # Read string from serial port and return it, timeout in seconds
  # Not really sure if we need to use select and getc
  #
  def read_string(timeout=2)
    str = String.new("")
    while(IO.select([@dev], nil, nil, timeout))
      str << @dev.getc
    end
    str.strip!
    
    if str.length > 0
      syslog("_ Wakey received packet: #{str}")
    end

    return str
  end


  #####
  # See if the Wakey board is functional
  # TODO: In a separate thread, attempt to read the serial number. Return nil if good.
  #
  def check_up
    nil  # Status is good
  end
  #####


  #####
  # Synonyms for read_string
  #
  def read_data(timeout=1.5)
    return nil unless (msg = self.get_msg(timeout))
    lines = msg.split("\n")
    tokens = lines[1].split
    return tokens
  end

  def get_msg(timeout=0.1)
    str = self.read_string(timeout)
    return str unless (str)

    ##
    # Replace newlines with spaces
    str.gsub!("\n"," ")
    str.gsub!("\r"," ")
    str = nil if (str == "")
    str
  end

  ##
  # Close the serial port.
  #
  def close
    syslog("_ Closing serial device for #{@name}")
    @dev.close
  end

  ##
  def flush_data
    #
    if (str = get_msg())
      syslog("_ #{@name} flushing #{str}")
      return true
    else
      syslog("_ #{@name} nothing to flush")
      return false
    end
  end

end
##########


##########
# WakeyLog specialized versions of DataLog class.
# Contains data from the wakey.
#
class WakeyLog < DataLog

  # Column headings for the log
  #
  @@cols = [
    "Battery Voltage (V)"
  ]

  def initialize(p_filename)
    super(p_filename)
    write_heading if @need_heading
  end

  def write_heading
    @file.write("SES Timestamp (ISO601),#{@@cols.join(',')}\n")
    @file.flush
  end
end
##########

##
# Standalone implementation
# Used so a process doesn't lock-up the wakey port. 
#
if __FILE__ == $0
  # Use a semaphore file to ensure serial access to the wakey port
  #
  File.new("/tmp/wakey.lock", File::CREAT || File::EXCL)
  
  # Get the command and execute if valid
  # Write the result to stdio
  #
  w = Wakey.new
  cmd = ARGV[0]

  # Is there a command in ARGV?
  #
  unless (cmd)
    puts("No command!")
    exit
  end
  
  # Is the command a valid Wakey method?
  #
  unless(w.respond_to?(cmd))
    puts "#{cmd}: unknown command!"
    exit
  end
  
  # Zero or one argument
  #
  if (ARGV[1])
    result = w.send(cmd, ARGV[1])
  else
    result = w.send(cmd)
  end
  puts(result)
end
