######################  thermal.rb -- brent@mbari.org  #####################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/thermal.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: thermal.rb,v 1.22 2005/11/04 22:29:21 brent Exp $
#
#     High-level control of heaters (and coolers)
#
############################################################################

require 'i2c/thermal'
require 'scale'

class Range
  def normalize
    low=self.begin
    high=self.end
    low < high ? self : high..low
  end
end

class Thermal < Scale

  class Error < Slide::Error; end
  
  class Exit < Error; end  #terminate thermal control thread without turning off heater
 
  def initialize(heaterName, dwarf, cfg=I2C::Thermal::Configuration.default,
                  maxDuration=60, slop=2, pollPeriod=1)
  #return a new heater object
  #dwarf is the network target that receives replies and sources requests
  #cfg is the device specific configuration
  #maxDuration is default max number of seconds to wait to reach desired temp
  #slop is the window around the goal that may be expressed as a range about 0
    coreInit heaterName
    @dwarf=dwarf
    @config=cfg
    @maxDuration = maxDuration
    @slop = slop
    @pollPeriod = pollPeriod  #poll temperature every poll seconds during a seek
  end
  attr_accessor :pollPeriod, :lastGoal
  attr_reader :thread       #nil or currently active monitoring/control thread
  undef_method :channel if respond_to? :channel     #there is only one / dwarf
  undef_method :home if respond_to? :home  #heaters don't have a home position
  
  def configure newCfg=nil
  #send configuration unconditionally
    @config=newCfg if newCfg
    Thread.log.recordMethod self, :configure, [@config]
    begin
      @dwarf.configure @lastConfigSent=@config.deepClone.freeze
    rescue =>err
      err.message[0,0]="Cannot configure #{@name}:  "
      raise
    end
  end
  
  def reconfigure newCfg=nil
  #send configuration only if it has changed
    @config=newCfg if newCfg
    configure unless @lastConfigSent.reallyEqual? @config
  end
  
  def amount
    rawAmount status.temperature
  end
  alias_method :temperature, :amount
  
  def getConfig
  #read configuration from the controller
    begin
      @dwarf.getConfig
    rescue =>err
      err.message[0,0]="Cannot read #{@name}'s configuration:  "
      raise
    end
  end
   
  def status
  #return the current raw status of the controller
    begin
      @dwarf.status
    rescue =>err
      err.message[0,0]="Cannot read #{@name}'s status:  "
      raise
    end
  end
    
  def rawTemperature
  #return the current raw position
    status.temperature
  end
  alias_method :rawPosition, :rawTemperature
  
  def rawProgram ignoredMaxDuration=5
  #return the current raw goal of the thermal program
  # or nil if no thermal program is running
    begin
      @dwarf.getProgram.program
    rescue =>err
      err.message[0,0]="Cannot read #{@name}:  "
      raise
    end
  end
  
  def rawGoal ignoredMaxDuration=5
  #return the current raw goal of the thermal program
  # or nil if no thermal program is running
    program=nil
    if tics = status.tics
      (program=rawProgram).each {|step|
        tics -= step.dwell
        return step.targetT if tics < 0
      }
    end
    raise Error.new("No Program running at this time!", self, program)
  end
    
  def accuracy slop=@slop
    (slop.is_a?(Range) ? slop : -slop..slop).normalize
  end
  
  def seek goal, maxDuration=@maxDuration, slop=@slop
  #heat/cool to the specified goal temperature within slop degrees C
  #in at most maxDuration seconds
    maxDuration=Delay.new maxDuration
    goal = asPosition(goal)
    prepare
    reconfigure
    args=[goal]
    args<<maxDuration unless maxDuration==@maxDuration
    args<<slop unless slop==@slop
    rawGoal = goal.raw
    if rawGoal > maxRaw or rawGoal < minRaw
      raise Error.new ("#{goal} would be out of bounds!", self, goal)
    end
    Thread.log.recordMethod self, :seek, args
    goalT = goal.temperature
    range = accuracy slop
    rawGoalRange = at(goalT+range.begin).raw..at(goalT+range.end).raw
    ignoreEnd = false
    Thread.critical=true
    begin
      @thread = Thread ("#{type}To#{rawGoal}".intern) {
        @lastGoal = goal
        begin
          reply = @dwarf.run I2C::Thermal::Step.default.
            with :targetT=>rawGoal,:dwell=>maxDuration/I2C::Thermal::TicSec
          err = reply.error
          errs = case err
            when nil
              Thread.exit if ignoreEnd
              "Could not reach #{goal} in " << maxDuration.to_str
            when :aborted
              Thread.exit if ignoreEnd
              err.to_s
            when :temperatureError
              "Failed seek to #{goal} after " <<
                 (reply.tics*I2C::Thermal::TicSec).seconds.to_str
            else
              err.to_s
          end
          err = Error.new errs, self, reply
        rescue Exit  #another thread wants us to quit gracefully
          Thread.exit
        rescue =>err
          err.message[0,0]="Running #{@name} program:  "
        ensure
          @thread=nil
        end
        err.raiseInAll Thread.parents
      }
      until rawGoalRange.include? ((stat=status).temperature)
        raise Error.new "Seek aborted", self, stat unless @thread.alive?
        Delay.sleep @pollPeriod
      end
    ensure
      ignoreEnd = true
    end
    self
  end

  def moveTo goal, maxDuration=@maxDuration, tmpCfg=nil, slop=@slop
  #like seek, but allows for a reconfiguration for just this move
    return seek goal, maxDuration, slop unless tmpCfg
    oldConfig = @config
    begin
      @config = tmpCfg
      result = seek goal, maxDuration, slop
    ensure
      @config = oldConfig
    end
    result
  end
  alias_method :to, :moveTo
  alias_method :heatTo, :to
  alias_method :coolTo, :to

  def hold duration, goal=@lastGoal, holdoff=duration, slop=@slop
  #hold temperature for duration seconds
  #goal defaults to the last temperature sought
  #if goal is nil, simply hold the current temperature
  #if goal is not supplied, hold the last temperature sought
  #returns the thread that will terminate when the hold time
  #has expired.  Raises exceptions in the calling thread on error.
    prepare
    reconfigure
    duration=Delay.new duration
    holdoff=Delay.new holdoff
    args = [duration]
    goal = temperature unless goal
    goal = asPosition goal
    args<<goal
    args<<holdoff unless holdoff == duration
    args<<slop unless slop==@slop
    Thread.log.recordMethod self, :hold, args
    rawGoal = goal.raw
    goalT=goal.temperature
    range = accuracy slop
    ts=I2C::Thermal::TicSec
    @thread = Thread ("#{type}Hold#{rawGoal}".intern) {
      begin
        reply = @dwarf.run I2C::Thermal::Step.default.
          with :targetT=>rawGoal,:dwell=>duration/ts,:errHoldoff=>holdoff/ts,
               :minT=>at(goalT+range.begin).raw,
               :maxT=>at(goalT+range.end).raw
        errs = case err=reply.error
          when nil
            Thread.exit
          when :temperatureError
            "Deviated from #{goal} after #{reply.tics*ts} seconds"
          else
            err.to_s
        end
        err=Error.new errs, self, reply
      rescue Exit  #another thread wants us to quit gracefully
        Thread.exit
      rescue =>err
        err.message[0,0]="Holding #{@name} program:  "
      ensure
        @thread=nil
      end
      err.raiseInAll Thread.parents
    }       
  end 

  def coast
  #let specified thermal unit coast
    Thread.log.recordMethod self, :stop
    prepare
    begin
      @dwarf.run I2C::Thermal::Step.default
    rescue =>err
      err.message[0,0]="#{@name} program:  "
      raise
    end
    self
  end
  alias_method :stop, :coast
  
  def cancel reason="<no reason given>"
  #signal any active control thread that it should exit
  #w/o generating an exception
    if t = @thread
      t.raise Exit.new reason, self
      t.finish
    end
    self
  end
  
  class Position < Scale::Position
    alias_method :temperature, :amount
  end

  protected
  def prepare
  #prepare to start a new thermal control thread    
    if oldThread = @thread
      parent = oldThread.parents.first
      cancel  #the old control thread
      #abort the parent of the old thread if if was not this thread
      parent.raise Thermal::Error.new "Aborted by another thread", self unless
         parent==Thread.current
    end
  end
      
end #Thermal class


