=begin
 ******************************************************************************
 * Copyright 1990-2007 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Interfaces to the Benthos acoustic modem
 * Filename : modem.rb
 * Author   : Henthorn
 * Project  : Benthic Rover
 * Version  : 2
 * Created  : Oct  2008
 * Modified : June 2012 - Added comments about register set-up.
 * Modified : Fall 2014 - removed setup commands as they were consuming power
 * Modified : Spring 2024 - fixed issues with command mode and online mode
 ******************************************************************************
 May 23, 2024 - simplify interface for reliability

 init phase  - instantiate object, open modem port, put in default online mode
 normal ops  - read data, write data in online mode
 special ops - send commands: command mode, issue command, back to online mode

 Commands: Send command with optional wait time for response (default one second).
           When a wait time is specified and no response is received in that
           time, the command is assumed to have failed (although it may not have).
 Examples: command("ats14=22\r")    # set register is almost instantaneous
           command("atr53\r", 10)   # range command can take up to 10 seconds
           command("at$x22,1\r", 3) # dev enable command can take up to 10 seconds


=end

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

require 'utils/misc'
require 'utils/datalog'
require 'rubygems'
require 'serialport'

# Benthos Modem commands and responses
#
# Modem register settings.
# These are written to the Rover modem using the init() method.
# We do not want to set these each time we instatiate a Modem object since
# that uses modem battery energy.
#
# Just set them up in the Rover modems and save them there.
#
$DOPPLER =       "ATS1=1\r"
$WAIT_TIME =     "ATS7=15\r"
$WRITE_DELAY =   "ATS8=10\r"
$IDLE_TIME =     "ATS10=1\r"
$VERBOSE_LEVEL = "ATS13=00\r"
$DEV_ENABLE    = "ATS28*2\r"
$WHAT_DEV_ENABLE = "ATS28\?\r"
$SAVE          = "AT&W\r"

$CMD_MODE =      "+++"
$ONLINE_MODE =   "ATO\r"
$CR_LF =         "\xd\xa"

$CMD_MODE_CONF = ">"
$ONLINE_CONF =   "bits/sec\xd\xa"
$CMD_CONF =      "OK"

##########
# BenthosModem class attempts to provide a useful interface to the
# acoustic modem, to set-up, read data and write data.
#
class BenthosModem
  include SyslogWriter

  attr_reader :dev, :name

  ##
  # Initializer usage: modem = BenthosModem.new(nil, "/dev/lfmodem_port")
  # Does not assume modem is powered-up.
  # Assume modem is in online mode by default
  #
  def initialize(portname='/dev/lfmodem')

    @name = portname
    @dev = SerialPort.new(portname) #, config)
    @dev.sync=true
    @rd = @wr = @dev    # read and write from modem port
    @cmd_mode = false
    @log = DataLog.new(DataLog.deployment_data_home + "/modem.csv")

    # Is there a default client?
    @default = false
  end

  ##
  # Set up a modem object for SES
  #
  def BenthosModem.ses
    m = BenthosModem.new('/dev/lfmodem')
  end

  OnlineFailed  = "no online"
  CommandFailed = "no command"
  NoComms       = "no comms"

  #####
  # Basic health check-up. Return true if passed, otherwise false.
  #
  def check_up
    ret = nil

    t = RoverThread.new do
      if (command_mode)
        syslog("_ check_up command_mode passed")
        if (online_mode)
          syslog("_ check_up online_mode passed")
          ret = nil
        else
          ret = OnlineFailed
        end
      else
        ret = CommandFailed
      end
    end
    sleep(10)

    if (t.alive?)
      t.kill
      t.join
      ret = NoComms
    end
    t.join

    ret
  end
  #####

  ##
  # Initialize modem using pre-defined constant commands.
  # This method should be called at the start of a deployment.
  # Does not assume modem is in "online" mode or command
  # mode.
  # No longer used by SES. Device setup is performed before
  # deployment and stored on the device.
  def init(cmd=nil)

    # Put modem in command mode
    #
    return false unless self.command_mode

    # If user sent in a command, only execute that command
    if (cmd != nil)
      command(cmd)
    else

      # Set verbose level, delays, and doppler correction
  #    return false unless command($VERBOSE_LEVEL)
  #    return false unless command($WRITE_DELAY)
  #    return false unless command($WAIT_TIME)
  #    return false unless command($DOPPLER)
      return false unless command($IDLE_TIME)
      return false unless command($DEV_ENABLE)
      return false unless command($SAVE)
    end

    # Set to online mode, ready for data transfer
    #
    return false unless self.online_mode

    return true
  end

  ##
  # Close the port
  #
  def close
    @dev.close
  end

  ##
  # Set the modem to command mode, and confirm
  # Method not intended for external use
  def command_mode
    syslog("_ Modem - setting command mode from #{@cmd_mode}")
    self.write($CMD_MODE)
    if nil == self.confirm($CMD_MODE_CONF)
      @cmd_mode = true
    else
      syslog("! Modem - setting command mode failed")
      @cmd_mode = false
    end
    return @cmd_mode
  end

  ##
  # Send a modem command to the modem, confirm that
  # command was carried-out
  #
  def command(cmdstr, timeout=1)
    return_val = false
    read_string
    command_mode unless @cmd_mode
    syslog("_ Modem - command \"#{cmdstr}\"")

    # Writing commands works best without the small delay
    # between each character used for writing data in online mode
    # Use the write port directly here.
    @wr.write(cmdstr)
    sleep(timeout)
    data = read_string(5)
    if  nil == self.confirm($CMD_MODE_CONF)
      return_value = data
    else
      syslog("!Modem - command (#{cmdstr}) unsuccessful")
      return_value = false
    end
    self.online_mode
    return return_value
  end

  ##
  # Set the modem to online mode, and confirm
  #
  def online_mode
    data = read_string()   # Clear the read buffer
    syslog("_ Modem - data in buffer: #{data}") if (data.length > 0)

    unless @cmd_mode
      syslog("_, Modem already in online mode")
      return true
    end

    self.write($ONLINE_MODE)

    @cmd_mode = false
    if (nil == self.confirm($ONLINE_CONF))
      syslog("_, Modem now in online mode")
      return true
    else
      syslog("_, Modem online mode cannot be confirmed")
      return false
    end

  end

  ##
  # Send a packet to the listening modem
  #
  # Modified to send the characters one at a time
  # to accomodate the SER1 board on the 7250-V2
  #
  def write(p)
    packet = p.to_s

    if @cmd_mode
      syslog("! Oops. Modem is in command mode")
    end

    syslog("_ Modem transferring packet: #{packet}")
    @log.write(name,packet)
    i = 0
    while (i < packet.length)
      @wr.write(packet[i])
      sleep(0.05)
      i += 1
    end
  end


  ##
  # Synonym for write
  #
  def send_msg(packet, send_anyway=true)
    if (@default || send_anyway)
      write(packet)
    end
  end

  ##
  # Read string from modem 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([@rd], nil, nil, timeout))
      str << @rd.getc
    end
    str.strip!

    if str.length > 0
      syslog("_ Modem received packet: #{str}")
      @log.write(name,str)
    end

    ##
    # Handle internal messages and handshakes here
    #
    if (str.strip.downcase == "default")
      @default = true
      self.send_msg("default ack", false)
      str = nil
    elsif (str.strip.downcase == "close")
      @default = false
      str = nil
    end

    return str
  end

  #####
  # Synonyms for read_string
  #
  def read_data(timeout=2)
    self.read_string(timeout)
  end

  def get_msg(timeout=0.1)
    str = self.read_string(timeout)
    str = nil if (str == "")
    str
  end


  # Read response from modem and confirm against expected response.
  # The confirm string will be compared to the LAST thing in the read buffer.
  # Return nil if equal, otherwise return the actual response.
  #
  def confirm(confstr)
    clen = confstr.length
    response = self.read_string

    return "" unless (response)

    # Compare the confirm string to the last part of the response
    #
    #token = response.slice(0-clen, clen)
    #if token == confstr
    if (nil != response.include?(confstr))
      return nil
    else
      syslog("Command unconfirmed. Expected (#{confstr}) but recv'd (#{response})")
      return response
    end
  end

  ##
  # Check to see if there is any data waiting to be read
  #
  def data_present?(timeout=0.1)
    return (nil != IO.select([@rd], nil, nil, timeout))
  end

  def data?(timeout=0.1)
    self.data_present?(timeout)
  end

end

##########
# Class SimModem creates a class that can be used in place of
# a BenthosModem class to simulate SES ops
#
class SimModem < BenthosModem
  include SyslogWriter

  ##
  # Use test streams for @dev
  #
  def initialize(portname="/dev/lfmodem")
    syslog("_ SimModem created")
    @cmd_mode = false
    @name = portname
    @default = true
  end

  def init()
    @log = DataLog.new("modem.csv")
    true
  end

  def close()
  end

  def check_up
    return nil
  end

  def write(packet)
    @log.write(name, packet)
  end

  def read_string(timeout=2)
    now = Time.now.to_i
    nil
  end

  def data_present?(to=0.1)
    nil
  end

end

##
# Standalone unit test (recommended)
#
if __FILE__ == $0
  # Test sim code here
  m = SimModem.new($stdin, $stderr)
  if (m.init)
    str = nil
    while ("quit" != str) do
      str = m.read_string
      puts(str) if (str)
    end
  end
  puts("Done")
end
