
class Sampler
#parametized sampling object
#  usage examples:
#   ThruCoarseFilter = Sampler.new 7..12, 1, 4
#   ThruCoarseFilter.sample 125 #sample 125 mls "through a coarse filter"

  class Error < RuntimeError; end
  class Clogged < Exception; end  #not a subclass of StandardError!
  class IntakeClogged < Clogged; end
  
  CanPSI      = 15  #Defined PSI in the sealed pressure housing
  Equalized   =  1  #treat pressures within 1 PSI as equal
  MinPSI      =  2  #don't rely on pulling below 2 PSI

  #configuration needs to define:
  #IntakeScaleAndBias = [y, b]  #first is scale, last is the bias at 0
  #OutletScaleAndBias = [y, b]
  
  def initialize hash={}  #for parametes passed by name
    @vacuum=7..10     #desired vacuum range across filter
    @endVacuum=3      #at end of stroke, wait til vacuum falls to this
    @maxDelay=40      #max secs to wait at end of stroke for endVacuum
    @minRate=.2       #minimum tolerable inhalation rate (mls/second)
    @maxResidue=.10   #maximum tolerable error before taking a short stroke
    @primeVolume=7    #mls with which to prime sampler
#   @primeDelay=10    #(don't) wait an extra 10 seconds at top of a prime stroke
    @exhaleConfig=SSexhale  #servo configuration for exhalations
    
    @evacDelay=15     #puckEvac delay in seconds
    @maxPumpVolume=40 #maximum puckEvac pump up volume in ml

    #hash of named parameters may override these defaults
    with hash
  end
  attr_accessor :vacuum, :endVacuum, :maxDelay, :minRate, :maxResidue
  attr_accessor :primeVolume, :primeDelay, :exhaleConfig
  attr_accessor :bubblePt, :evacDelay, :maxPumpVolume
  
  attr_reader   :outletVacuum, :vacuum
  
  def vacuum= newVac
    newVac=newVac.last..newVac.first if newVac.first > newVac.last
    @vacuum=newVac
  end

  def self.ambient
    @@ambientPress if defined? @@ambientPress
  end
    
  def self.ambientIntake
    @@ambientPress.first
  end
  
  def self.ambientOutlet
    @@ambientPress.last
  end
  
  def self.outLessIn
    ambientOutlet - ambientIntake
  end
  
  #override next three methods if pressure sensor response is non-linear
  def self.intakePress
  #return the relative intake pressure in PSI
   CS.status.outPress*IntakeScaleAndBias.first - IntakeScaleAndBias.last
  end
  def self.outletPress
  #return the relative outlet pressure in PSI
   SS.status.outPress*OutletScaleAndBias.first - OutletScaleAndBias.last
  end
  def outletPSItoCounts adcCounts
  #return raw outlet pressure counts for given pressure in PSI
    (adcCounts+OutletScaleAndBias.last)/OutletScaleAndBias.first
  end


  def self.intakePSI inPress = intakePress
  #return (calibrated) intake pressure in absolute PSI
    inPress - ambientIntake + CanPSI
  end
  def self.outletPSI outPress = outletPress
  #return (calibrated) outlet pressure in absolute PSI
    outPress - ambientOutlet + CanPSI
  end
  
  def self.forgetCalibration
  #force recalibration of pressure sensor bias before taking next sample
    @@ambientPress = nil
    self
  end
  
  def self.inhaleConfig= config
    @@inhaleConfig = config.dup
    @@inhaleConfig.intern=:forSampling
  end
  
  def self.inhaleConfig
    self.inhaleConfig = SSinhale unless defined? @@inhaleConfig
    @@inhaleConfig
  end
  
  def self.calibrate
  #calculate the constant offset between the SS and CS pressure sensors
  #assumes that a dry collection puck is clamped 
    Log.record "Calibrating pressure sensors"
    CTV.to :air    
    SSV.to :spit
    SS.to 5, SSfast
    SSV.connect CBV
    Delay.sleep 2
    # [puck intake pressure counts, puck outlet pressure counts]
    @@ambientPress=[intakePress, outletPress]
    SSV.to :spit  #maybe push this slug of wet air to waste instead?
    SS.to 0, SSfast
    SSV.to :CBV
  end

  def self.ready?
    FlushPuck.ready? and not (CS.status.lost or SS.status.lost)
  end
  def self.ready!
    Intake.close
    Exhaust.close
    CTV.to :air
    CSV.connect CBV
    SSV.to :spit
    rdyFlush = Thread :readyFlushPuck do
      FlushPuck.ready! unless FlushPuck.ready?
    end
    rdyCS = Thread :readyCS do
      CS.home.to 0, CSfast
    end
    SS.home.to 0
    rdyFlush.join
    rdyCS.join    
  end
  

  def self.purge sampleVol=25, flushVol=5, airVol=3
  #assumes the intake is at ambient pressure 
    raise Error, (CS.name+" not empty") if CS.volume > .25
    sample sampleVol
    CTV.to :air
    CSV.to :flush
    CS.to flushVol
    delay 5
    CSV.to :air
    CS.pull airVol, CSfast
    CSV.connect CBV
    CS.to 0, CSfast
    delay 7
    CS.to 3, CSfast
    CSV.to :waste
    CS.to 0, CSfast
  end

  def self.pressureStroke backPSI=intakePSI, pressurize=CSpressurize
  #do one pressurization stroke
  #return true if target pressure reached
    if (compressionRatio = backPSI / Float(Sampler::CanPSI)) > 1.075
      CSV.dialBetween :air, :CTV
      #slightly pressurize CS relative to intake
      CS.to (CS.maxVolume / (compressionRatio+0.1), CSfast)
    end
    CSV.to :CTV
    begin
      CS.to 0, pressurize      #pressurize to break filter bubble point
    rescue Slide::Error => pressureErr
      case pressureErr.reply.error
        when :overPressure
          return true
        else
          raise  #any other servo error aborts
      end
    end
    CSV.to :air
    false
  end

  def exhale
  #empty the sampler syringe to exhaust
    SSV.to :exhaust
    Exhaust.open
    begin
      SS.to 0, @exhaleConfig
    rescue Slide::Error => pushErr
      case pushErr.reply.error
        when :overCurrent, :positionErr, :overPressure
          Log.recordException pushErr
          SS.to 0, SSslow  #one last try at a low speed
        else
          raise
      end
    ensure
      Exhaust.close
    end
  end

  def setupStroke
  #set up pressure parameters to take a sample stroke
  #returns ambient intake pressure in counts    
    maxVacuum = type.intakePSI (intake=type.intakePress) - MinPSI
    raise IntakeClogged.new "Intake Blocked" if maxVacuum <= 0
    @outletVacuum = 
      if @vacuum.last > maxVacuum  #do not try to pull a full vacuum!
        minVacuum = maxVacuum - (@vacuum.last - @vacuum.first)
        minVacuum = Equalized if minVacuum < Equalized
        minVacuum..maxVacuum
      else
        @vacuum.dup     
      end
    intake
  end
  
  def equalize slop=@endVacuum, pollInterval=1,
                  errMsg="Intake @%dpsi & Outlet @%dpsi won't equalize"
  #wait for outlet pressure to get within slop PSI of intake
    endTime = Schedule.now + @maxDelay
    while Schedule.now < endTime  #wait for inlet & outlet pressures to equalize
      Delay.sleep pollInterval
      intake = type.intakePSI
      outlet = type.outletPSI
      return endTime if intake - outlet <= slop
    end
    raise Clogged.new (errMsg % [intake, outlet])
  end
  
  def inhale strokeVolume
  #carefully pull to strokeVolume
  #raise Clogged if stroke cannot be completed due to clogged filter
    retries=3
    reportedIntakePress=nil
    config = type.inhaleConfig
    begin
      intakePress = setupStroke
      config.minOutPress = outletPSItoCounts (intakePress + type.outLessIn - @outletVacuum.last)
#breakpoint "inhalation begins..."
      if reportedIntakePress != intakePress
        inPSI = type.intakePSI (reportedIntakePress=intakePress)
        Log.record "Intake @#{inPSI}psi, Puck vacuum #{@outletVacuum}psi"          
      end
      SS.to strokeVolume, config
    rescue Slide::Error => vacuumErr
      case vacuumErr.reply.error
        when :overPressure
print "Puck Vacuum too high -- waiting up to #{@maxDelay}s ..."
          endTime = equalize @outletVacuum.begin, .5,
                      "Intake @%dpsi, & Outlet @%dpsi -- no progress"
          puts " continuing after #{@maxDelay-(endTime-Schedule.now)}s" 
          slower=config.maxSpeed - 30   #slow down syringe rate after each error
          config.maxSpeed = slower if slower >= SSslow.maxSpeed
          retry

        when :positionErr, :overCurrent
          raise if retries <= 0
          Log.recordException vacuumErr
          retries = retries - 1
          config=SSslow  #slow to slowest stable speed for retries
          retry

        else
          raise
      end
    end
puts "Stroke completed -- waiting for pressure to equalize ..."
    equalize
  end
  private :inhale

 
  def sampleQuietly volume, extraStrokeDelay=nil
  #collect specified sample volume via a series of inhales and exhales
  #return the actual volume sampled (may be less than that specified)
    Intake.close
    Exhaust.close
    if SS.volume >= @maxResidue
      exhale
    else
      SS.hold
    end
    raise Error, "Collection Clamp is not closed!" unless CC.closed?
    type.calibrate unless type.ambient
    maxStroke = SS.maxVolume
    residue = 0
    volumeLeft = volume
    
    CTV.dial :intake
    clogged = false
    while not clogged and volumeLeft > residue
      SSV.connect CBV
      strokeVolume = volumeLeft > maxStroke ? maxStroke : volumeLeft
      if @minRate > 0
        maxStrokeDuration = strokeVolume/@minRate + 5
        Log.record "Allowing %ds for %.1fml of remaining %.1fml" % 
                             [maxStrokeDuration+.5, strokeVolume, volumeLeft]
      else
        maxStrokeDuration = nil
        Log.record "%.1fml of remaining %.1fml" % [strokeVolume, volumeLeft]
      end  
      alarm=nil
      begin
        alarm=(Delay.raiseAfter maxStrokeDuration,
                Clogged.new "progress too slow" if maxStrokeDuration)
        Intake.open
        Delay.sleep 2   #allow intake pressure to equalize after opening valve
        inhale strokeVolume
      rescue Clogged => clogged
        SS.hold
      rescue Exception
        begin
          SS.hold
        rescue  #ignore errors stopping syringe
        end
        Intake.close
        raise   
      ensure
        alarm.cancel if alarm
      end
      Intake.close
      strokeVolume = SS.volume
      volumeLeft -= strokeVolume if strokeVolume > 0
      if clogged
        clogged.message << (" after %.1fml" % (volume - volumeLeft))
        Log.recordException clogged
      end
      delay extraStrokeDelay if extraStrokeDelay
      exhale
      residue = @maxResidue if residue < @maxResidue
    end
#    SS.coast  #good idea, but causes SS to bounce in practice
    volume - volumeLeft
  end

  def sample vol
    Log.record "Sampling %.1fml" % vol
    sampledVol = sampleQuietly vol
    Log.record "Sampled  %.1fml" % sampledVol
    sampledVol
  end
      
  def prime vol=@primeVolume, delay=@primeDelay
    Log.record "Priming #{vol}ml"
    raise Clogged.new "while priming" if sampleQuietly(vol, delay) < .8*vol
    self
  end

  def spitAndEqualize
  #spit out SS contents and equalize pressure between it and puck
    SSV.to :spit
    SS.to 0, @exhaleConfig
    Delay.sleep .5
    SS.to 5, SSfast
    SSV.connect CBV
  end
  private :spitAndEqualize


  def puckEvac
  #evacuate residual liquid from the sample puck
  #bubblePt should be slightly higher than the nominal filter bubble pt in PSI

    raise Error, "Unspecified filter bubble point (psi)" unless @bubblePt
    Log.
      record "Evacuating Puck (bubblePt=#{@bubblePt}psi, delay=#{@evacDelay}s)"

    Intake.close

    airStroke = Thread :airStroke do
      CSV.to :waste
      CS.to 0, CSfast
      CSV.to :air       #get started with first stroke of air
      CS.fill CSfast
    end if @bubblePt > 0

    exhale if SS.volume > 0
    type.calibrate unless type.ambient

    SSV.connect CBV

    CTV.to :intake   #is this really necessary??
    SS.to 8
    
#breakpoint "After 1st SS pull"

    CTV.to :air
    delay @evacDelay
    SSV.to :spit
    prepCSV = Thread :prepCSV do
      CTV.to :CSV, :up  #ensure that CTV does not move through intake!
    end
    SS.to 0, @exhaleConfig
    SSV.to :CBV
    prepCSV.join

    if @bubblePt > 0  #pump air via CS to develop bubblePt PSI across the filter
      airStroke.join
      #more scott shit code reconvert to dwarf psi
      CSpressurize.maxOutPress = ((@bubblePt - type.outLessIn + type.outletPress)+52.3283)/.863977 
      volPumped = 0.0
      loop do 
        break if type.pressureStroke
        if (volPumped += CS.maxVolume) >= @maxPumpVolume
          raise Error,
            "Could not reach #{@bubblePt}psi after pumping #{@maxPumpVolume}ml"
        end
        CS.fill CSfast
      end
      inPSI = type.intakePSI
      outPSI = type.outletPSI
      Log.record "BubblePoint: Intake @#{inPSI}psi, Outlet @#{outPSI}psi"
    end

#breakpoint "Pressurized"

    pullAir = Thread :CSpull do
      CSV.dial :air
      CS.to 3, CSfast  #provide an air buffer air to equalize between CS and CTV
    end

    SS.to 10   #remove seawater from puck
    delay @evacDelay

#breakpoint "Depressurized"

    spitAndEqualize
    
    CTV.to :air, :down
    emptyCS = Thread :CSpush do
      pullAir.join
      CSV.to :CTV   #equalize pressure between CS and CTV
      Delay.sleep 1
      CSV.to :air
      CS.to 0, CSfast
      CS.coast
    end

    SS.to 10

    spitAndEqualize

    Delay.sleep 1
    SSV.to :spit
    SS.to 0
    SS.coast
    emptyCS.join
  end

end


def sample volume, hash={}
#create a temporary Sampler object and use it
  Sampler.new(hash).sample volume
end

def prime volume=nil, hash={}
#create a temporary Sampler object and use it
  mySampler = Sampler.new hash
  mySampler.primeVolume = volume if volume
  mySampler.prime
end

def exhale hash={}
#empty the sampler syringe to exhaust
  Sampler.new(hash).exhale
end

def puckEvac bubblePt=nil, hash={}
#evacuate the puck
  evacSampler = Sampler.new hash
  evacSampler.bubblePt = bubblePt if bubblePt
  evacSampler.puckEvac
end

