#####################  solenoid.rb -- brent@mbari.org  ####################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/solenoid.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: solenoid.rb,v 1.25 2005/11/03 23:14:57 brent Exp $
#
#     Rotary Shaft control
#
#  This layers on top of I2C/Solenoid to define operations on 
#  solenoid actuators (typically valves)
#
############################################################################

require 'i2c/solenoid'
require 'axismap'

class Solenoid < AxisKernel

  class Error < StandardError; end
  
  class Valves < RMutex
  #link all solenoids on a given dwarf together with their shared configuration
  
    unless defined?(@@defined) and defined?(@@present)
      @@defined = []  #all defined Solenoid::Dwarves
      @@present = []  #all present Solenoid::Dwarves
    end
    
    def initialize controller, pulseSecs
    #start a new set of valves on specified controller with pulseWidth in secs
      @ch = []    #clear its array of solenoid channels
      super()
      @pulseWidth=pulseSecs
      @dwarf=controller
      @@defined<<self
    end
    def assign solenoid
    #associate solenoid channel with this dwarf
      @ch[solenoid.channel] = solenoid
    end

    def [] channel
      @ch[channel]
    end
        
    def configure
    #assign dwarf's solenoid types and its pulseWidth
      begin
        begin
          @dwarf.configure(@pulseWidth,
             @ch.collect {|solenoid| solenoid ? solenoid.kind : :nonlatching})
        rescue =>err
          name = intern
          name = "valves" if !name or name == self
          err.message[0,0]="Cannot configure #{name}:  "
          raise
        end
        @@present<<self unless @@present.include? self
        updateState
      rescue Msg::NodeOffline => offline
        Thread.log.recordException offline
        valves=[]
        @ch.each {|valve| valves<<valve.name.to_s if valve}
        Thread.log.record ("MISSING VALVES ["<<(valves.join ', ')<<']')
      end
    end
    attr_reader :dwarf, :channel  #mainly for debugging
    
    def address
      @dwarf.address
    end
    
    def update
      #returns settledEv or nil if none
      lock {
        ev = @dwarf.update(@ch.collect{|solenoid|
          solenoid.clearGoal if solenoid
        })
        if ev
#          @@present[self] = ev
          updateState
        end
        ev
      }
    end

    def forget solenoids=nil
    #forget the state of an array of this dwarf's solenoid channels
      @dwarf.forget solenoids
      updateState
    end

     
    def present?
      @@present.include? self
    end
    def defined?
      @@defined.include? self
    end
    
    def self.reconfigureAll
    #reconfigure all present Solenoid::Dwarves
      @@present.each{|valves| 
        valves.configure
      }
    end
    
    def self.configureAll
    #configure all defined Solenoid::Dwarves
      @@present=[]
      @@defined.each{|valves| 
        valves.configure
      }
    end
    
    def self.updateAll
    #update all Solenoid::Dwarves
    #returns array of pending settled events
      result=[]
      @@present.each{|valves| 
        settleEv = valves.update
        result << settleEv if settleEv
      }
      result
    end
    
    def self.forgetAll
    #forget current state of all solenoids
      @@present.each{|valves| valves.forget}
    end
    
    def status
      lock {
        reply = @dwarf.status
        updateState
        reply
      }
    end
    
    def updateState
      state = @dwarf.state
      for i in 0...state.size  #update solenoid states to match dwarf's
        if ch = @ch[i]
          s = state[i]
          ch.setRawState s if s or ch.kind == :nonlatching
        end
      end
    end
    
    def each(&block)
    #pass each valve in valves to block in turn
      @ch.each{|ch| block[ch] if ch}
    end
    
    def self.each(&block)
    #pass every valve to block in turn
      @@present.each{|valves| valves.each &block }
    end
  end  #Solenoid::Valves class
  
  class Moving
  #represent a solenoid's transition between one state and another
    def initialize goal
      @goal = goal
    end
    def to_s
      "moving to #{@goal}"
    end
    alias_method :asIRBtext, :to_s
    def intern
      @goal.intern
    end
  end
  @@transitions = {:relaxing=>:off, :rising=>:up, :falling=>:down}
  
  class Latching < AxisMap
    def kind
      :latching
    end
  end
  class NonLatching < AxisMap
    def kind
      :nonlatching
    end
  end
  
  def kind
    @map.kind
  end
  
  def initialize(solenoidName, valves, solenoidNumber, solenoidMap)
  #return a new Solenoid object
  #solenoidMap names the solenoid's states and defines its "home" state
  #valves is a Solenoid::Valves object
  #solenoidNumber is the solenoid channel (typically 0..7)
    @name = solenoidName
    @map = solenoidMap
    @channel = solenoidNumber
    @state = :off
    (@valves = valves).assign self
    type.define self
  end
  attr_accessor :name
  attr_reader :map, :valves, :channel

  def configure
  #throw AxisKernel::Missing if the corresponding Solenoid::Valves not present
    raise AxisKernel::Missing.new (
            "Missing #{@name}", self) unless @valves.present?
  end
  
  def rawId rawPos
  #convert solenoid's rawPosition to logical position
    @map.rawId (rawPos) {|raw|
      Moving.new (@map.rawId(@@transitions.fetch(raw) {|raw|
        raise Error,"Unhandled #{name} #{type} Hardware State: #{raw}"
      }))
    }
  end
  
  def state
    @valves.lock {
      @valves.updateState if I2C::Solenoid::Transition[@state]
      rawId @state
    }
  end

  def forget
    @valves.forget [@channel]
    self
  end
   
  def rawGoal
    @newState
  end
  def clearGoal
    g = @newState
    @newState = nil
    g
  end
    
  def goal
    rawId @newState if @newState
  end
  
  def goal= goalState
    @newState = (@map.fetch(goalState) {|goal|
      raise Error,"Invalid #{name} #{type} State: #{goal}"
    } if goalState)
  end
  alias_method :state=, :goal=
   
  def legend
    @map.legend
  end
  
  def wiring
    "#{@valves.intern}[#{channel}]"
  end

  def setRawState rawState
    rawState = :off unless rawState
    @state=rawState
  end
  def getRawState
    @state
  end
  
  def to_s
  #convert current state to a descriptive string
    s = state
    return "in an unknown state" unless s
    s.to_s
  end
  def asIRBtext
    "#{@name} #{type} #{self}"
  end

  def select goalState
    Thread.log.recordMethod self, :select, [goalState]
    self.goal=goalState
    if settleEv = @valves.update
      Delay.sleepUntil (settleEv.time+.001)
    end
    self
  end
  alias_method :to, :select
  
  #any undefined method is interpreted as a goal symbol!
  alias_method :method_missing, :select
  
  def reset
  #move to the home state immediately
    select :home
  end
  
  def home
  #move to the home state after the next update
    self.goal=:home
    self
  end
  

  class << self  #class methods    
    def settle
    #update all solenoids and wait for them to settle
      return 0 if (evs=Valves::updateAll).empty?
      Delay.sleepUntil ((evs.max{|x,y|x.time<=>y.time}).time + .001)
      evs.size
    end

    def home
      each {|valve| valve.home}
      self
    end
          
    def reset
      Thread.log.recordMethod self, :reset
      home
      settle
      self
    end

    def forget
    #forget all valve states
      Valves::forgetAll
      self
    end
    
  end #class methods
  
end #Solenoid class
