示例#1
0
    def __init__(self, tools, publisher, dir_path):
        """Initialize Head object
        
        tools = dictionary of the tools on the head
        
        """
        self.smoothieAPI = openSmoothie.Smoothie(self)
        self.PIPETTES = {'a':Pipette('a'),'b':Pipette('b')}    #need to create this dict in head setup
        self.tools = tools
        self.pubber = publisher
        self.smoothieAPI.set_raw_callback(self.pubber.on_raw_data)
        self.smoothieAPI.set_position_callback(self.pubber.on_position_data)
        self.smoothieAPI.set_limit_hit_callback(self.pubber.on_limit_hit)
        self.smoothieAPI.set_move_callback(self.pubber.on_start)
        self.smoothieAPI.set_delay_callback(self.pubber.show_delay)
        self.smoothieAPI.set_on_connect_callback(self.pubber.on_smoothie_connect)
        self.smoothieAPI.set_on_disconnect_callback(self.pubber.on_smoothie_disconnect)
        self.theQueue = TheQueue(self, publisher)
        
        self.path = os.path.abspath(__file__)
        self.dir_path = dir_path  
        self.dir_par_path = os.path.dirname(self.dir_path)
        self.dir_par_par_path = os.path.dirname(self.dir_par_path)      

        self.load_pipette_values()
示例#2
0
class Head:
    """A representation of the robot head
    
    The Head class is intended to be instantiated to a head object which
    aggregates the subclassed tool objects and the smoothieAPI object.
    It also hold a references to theQueue and publisher objects.
    Appropriate methods are exposed to allow access to the aggregated object's
    functionality.
    """
    
#Special Methods-----------------------
    #def __init__(self, tools, global_handlers, theQueue):
    def __init__(self, tools, publisher, dir_path):
        """Initialize Head object
        
        tools = dictionary of the tools on the head
        
        """
        self.smoothieAPI = openSmoothie.Smoothie(self)
        self.PIPETTES = {'a':Pipette('a'),'b':Pipette('b')}    #need to create this dict in head setup
        self.tools = tools
        self.pubber = publisher
        self.smoothieAPI.set_raw_callback(self.pubber.on_raw_data)
        self.smoothieAPI.set_position_callback(self.pubber.on_position_data)
        self.smoothieAPI.set_limit_hit_callback(self.pubber.on_limit_hit)
        self.smoothieAPI.set_move_callback(self.pubber.on_start)
        self.smoothieAPI.set_delay_callback(self.pubber.show_delay)
        self.smoothieAPI.set_on_connect_callback(self.pubber.on_smoothie_connect)
        self.smoothieAPI.set_on_disconnect_callback(self.pubber.on_smoothie_disconnect)
        self.theQueue = TheQueue(self, publisher)
        
        self.path = os.path.abspath(__file__)
        self.dir_path = dir_path  
        self.dir_par_path = os.path.dirname(self.dir_path)
        self.dir_par_par_path = os.path.dirname(self.dir_par_path)      

        self.load_pipette_values()
        
    def __str__(self):
        return "Head"
        
    def __repr__(self):
        return "Head({0!r})".format(self.tools.keys())
        
# the current coordinate position, as reported from 'smoothie.js'
    theState = {'x' : 0,'y' : 0,'z' : 0,'a' : 0,'b' : 0}

    # this function fires when 'smoothie.js' transitions between {stat:0} and {stat:1}
    #SMOOTHIEBOARD.on_state_change = function (state) {
    def on_state_change(self, state):
        """Check the given state (from Smoothieboard) and engage :obj:`theQueue` (:class:`the_queue`) accordingly

        If the state is 1 or the state.delaying is 1 then :obj:`theQueue` is_busy,

        else if the state is 0 and the state.delaying is 0, :obj:`theQueue` is not busy, 
        clear the currentCommand for the next one, and if not paused, tell :obj:`theQueue` 
        to step. Then update :obj:`theState`.

        :todo:
        :obj:`theState` should be updated BEFORE the actions taken from given state
        """
        
        if state['stat'] == 1 or state['delaying'] == 1:
            self.theQueue.is_busy = True

        elif state['stat'] == 0 and state['delaying'] == 0:
            self.theQueue.is_busy = False
            self.theQueue.currentCommand = None
            if self.theQueue.paused==False:
                self.theQueue.step(False)
    
        self.theState = state


#local functions---------------
    def get_tool_type(self, head_tool):
        """Get the tooltype and axis from head_tool dict
        
        :returns: (tool_type, axis)
        :rtype: tuple
        """
        tool_type = head_tool['tool']
        axis = head_tool['axis']
        
        return (tool_type, axis)
        
        
        
#Methods-----------------------
    def configure_head(self, head_data):
        """Configure the head per Head section of protocol.json file
        
        
        :example head_data:

        head_data = dictionary of head data (example below):
            "p200" : {
                "tool" : "pipette",
                "tip-racks" : [{"container" : "p200-rack"}],

                ... or ...
                "tip-racks" : ["p200-rack","some-other-rack"]  <--- preferred array format for JSON


                "trash-container" : {"container" : "trash"},


                ... or ...
                "trash-container" : ["trash"] <--- preferred array format for JSON (although currently only one trash container supported)

                "tip-depth" : 5,
                "tip-height" : 45,
                "tip-total" : 8,
                "axis" : "a",
                "volume" : 160
            },
            "p1000" : {
                "tool" : "pipette",
                "tip-racks" : [{"container" : "p1000-rack"}],
                "trash-container" : {"container" : "trash"},
                "tip-depth" : 7,
                "tip-height" : 65,
                "tip-total" : 8,
                "axis" : "b",
                "volume" : 800
            }
        """
        #delete any previous tools in head
        del self.tools
        self.tools = []
        #instantiate a new tool for each name and tool type in the file
        #ToDo - check for data validity before using

        for key in head_data:
            hd = head_data[key]
            #get the tool type to know what kind of tool to instantiate
            tool_type = self.get_tool_type(hd)  #tuple (toolType, axis)
            if tool_type[0] == 'pipette':
                #newtool = Pipette(hd['axis'])
                #pass
                #self.PIPETTES[hd['axis']] = newtool
                setattr(self.PIPETTES[hd['axis']],'tip_racks',hd['tip-racks'])
                if len(hd['tip-racks'])>0:
                    tpOD = hd['tip-racks'][0]
                    if isinstance(tpOD,dict):
                        tpItems = tpOD.items()
                        listTPItems = list(tpItems)
                        setattr(self.PIPETTES[hd['axis']],'tip_rack_origin',listTPItems[0][1])
                    elif isinstance(tpOD,str):
                        setattr(self.PIPETTES[hd['axis']],'tip_rack_origin',tpOD)



                setattr(self.PIPETTES[hd['axis']],'trash_container',hd['trash-container'])
                if 'tip-depth' in hd:
                    setattr(self.PIPETTES[hd['axis']],'tip-depth',hd['tip-depth'])
                if 'tip-height' in hd:
                    setattr(self.PIPETTES[hd['axis']],'tip-height',hd['tip-height'])
                if 'tip-total' in hd:
                    setattr(self.PIPETTES[hd['axis']],'tip-total',hd['tip-total'])
                if 'axis' in hd:
                    setattr(self.PIPETTES[hd['axis']],'axis',hd['axis'])
                if 'volume' in hd:
                    setattr(self.PIPETTES[hd['axis']],'volume',hd['volume'])

        self.save_pipette_values()
        self.publish_calibrations()


    def relative_coords(self):
        for axis in self.PIPETTES:
            self.PIPETTES[axis].relative_coords()
        self.save_pipette_values()
        self.publish_calibrations()
        
    #this came from pipette class in js code
    def create_pipettes(self, axis):
        """Create and return a dictionary of Pipette objects

        :returns: A dictionary of pipette objects
        :rtype: dictionary
        
        :note: Seems nothing calls this...

        :todo:
        Is :meth:`create_pipettes` even needed?
        """
        thePipettes = {}
        if len(axis):
            for a in axis:
            #for i in range(0,len(axis)):
                #a = axis(i)
                thePipettes[a] = Pipette(a)
                
        return thePipettes
        
        
    #Command related methods for the head object
    #corresponding to the exposed methods in the Planner.js file
    #from planner.js
    def home(self, axis_dict):   #, callback):
        """Home robot according to axis_dict
        """
        #maps to smoothieAPI.home()
        
        self.smoothieAPI.home(axis_dict)
        
        
    #from planner.js
    def raw(self, string):
        """Send a raw command to the Smoothieboard
        """
        #maps to smoothieAPI.raw()
        #function raw(string)
        self.smoothieAPI.raw(string)
        
        
    #from planner.js
    def kill(self):
        """Halt the Smoothieboard (M112) and clear the the object (:class:`the_queue`)
        """
        #maps to smoothieAPI.halt() with extra code
        self.smoothieAPI.halt()
        self.theQueue.clear();

    #from planner.js
    def reset(self):
        """Reset the Smoothieboard and clear theQueue object (:class:`the_queue`)
        """
        #maps to smoothieAPI.reset() with extra code
        self.smoothieAPI.reset()
        self.theQueue.clear();
        
        
    #from planner.js
    def get_state(self):
        """Get state information from Smoothieboard
        """
        #maps to smoothieAPI.get_state()
        #function get_state ()
        return self.smoothieAPI.get_state()
        
        
        #from planner.js
    def set_speed(self, axis, value):
        """Set the speed for given axis to given value
        """
        
        #maps to smoothieAPI.set_speed()
        #function setSpeed(axis, value, callback)
        self.smoothieAPI.set_speed(axis, value)
        
        
        #from planner.js
        #function move (locations)
        #doesn't map to smoothieAPI
    def move(self, locations):
        """Moves the head by adding locations to theQueue



        var locations = [location,location,...]

        var location = {
        'relative' : true || false || undefined (defaults to absolute)
        'x' : 30,
        'y' : 20,
        'z' : 10,
        'a' : 20,
        'b' : 32
        }

        """
        if locations:
            self.theQueue.add(locations)
        
    #from planner.js
    #function step (locations)
    #doesn't map to smoothieAPI
    def step(self, locations):
        """Step to the next command in theQueue(:class:`the_queue`) object's qlist

        
        locations = [location,location,...]

        location = {
        'x' : 30,
        'y' : 20,
        'z' : 10,
        'a' : 20,
        'b' : 32
        }
        """
        if len(self.theQueue.qlist)==0: # and self.theQueue.is_busy==False:

            if locations is not None:
                if isinstance(locations,list):
#                    for( i = 0; i < locations.length; i++):
                    for i in range(len(locations)):
                        locations[i]['relative']  = True
                        
                elif ('x' in locations) or ('y' in locations) or ('z' in locations) or ('a' in locations) or ('b' in locations):
                    locations['relative']  = True
                    
                self.move(locations)
         
         
    #from planner.js
    #function pipette(group)
    def pipette(self, group):
        """Run a pipette operation based on a given Group from protocol instructions


        group = {
          command : 'pipette',
          axis : 'a' || 'b',
          locations : [location, location, ...]
        }
    
        location = {
          x : number,
          y : number,
          z : number,
          container : string,
          plunger : float || 'blowout' || 'droptip'
        }
    
        If no container is specified, XYZ coordinates are absolute to the Smoothieboard
        if a container is specified, XYZ coordinates are relative to the container's origin 
        (180 degree rotation around X axis, ie Z and Y +/- flipped)
        
        """
        if group and 'axis' in group and group['axis'] in self.PIPETTES and 'locations' in group and len(group['locations'])>0:
    
            this_axis = group['axis']  
            current_pipette = self.PIPETTES[this_axis]  
    
            # the array of move commands we are about to build from each location
            # starting with this pipette's initializing move commands
            move_commands = current_pipette.init_sequence()
    
            # loop through each location
            # using each pipette's calibrations to test and convert to absolute coordinates
            for i in range(len(group['locations'])) :
    
                thisLocation = group['locations'][i]  
    
                # convert to absolute coordinates for the specifed pipette axis
                absCoords = current_pipette.pmap(thisLocation)  
    
                # add the absolute coordinates we just made to our final array
                move_commands.extend(absCoords)  
    
            if len(move_commands):
                move_commands.extend(current_pipette.end_sequence())  
                self.move(move_commands)  
      
    
    #from planner.js
    def calibrate_pipette(self, pipette, property_):
        """Sets the value of a property for given pipette by fetching state information 
        from smoothieboard(:meth:`smoothie_pyserial.get_state`)
        """
        #maps to smoothieAPI.get_state() with extra code
        if pipette and self.PIPETTES[pipette]: 
            state = self.smoothieAPI.get_state()
            if property_=='top' or property_=='bottom' or property_=='blowout' or property_=='droptip':
                value = state[pipette]
                self.PIPETTES[pipette].calibrate(property_,value)  
                self.save_pipette_values()


    def calibrate_container(self, pipette, container):   
        """Set the location of a container
        """
        if pipette and self.PIPETTES[pipette]:     
            state = self.smoothieAPI.get_state()
            self.PIPETTES[pipette].calibrate_container(container,state)

             
    def save_volume(self, data):
        """Save pipette volume to otone_data/pipette_values.json
        """
        logger.debug('saved volume: {}'.format(data['volume']))
        if(self.PIPETTES[data['axis']] and data['volume'] is not None and data['volume'] > 0):
            self.PIPETTES[data['axis']].volume = data['volume']
            
        self.save_pipette_values()

        self.publish_calibrations()
        
        
    #from planner.js
    def save_pipette_values(self):
        """Save pipette values to otone_data/pipette_values.json
        """
        pipette_values = {}

        params_to_save = [
            'resting',
            'top',
            'bottom',
            'blowout',
            'droptip',
            'volume',
            'theContainers',
            'tip_racks',
            'trash_container',
            'tip_rack_origin'
        ]

        for axis in self.PIPETTES:
            pipette_values[axis] = {}
            for k, v in self.PIPETTES[axis].__dict__.items():
                # make sure we're only saving what we need from that pipette module
                if k in params_to_save:
                    pipette_values[axis][k] = v

            # should include:
            #  'top'
            #  'bottom'
            #  'blowout'
            #  'droptip'
            #  'volume'
            #  'theContainers'

        filetext = json.dumps(pipette_values,sort_keys=True,indent=4,separators=(',',': '))
        
        filename = os.path.join(self.dir_path,'otone_data/pipette_calibrations.json')

        # save the pipette's values to a local file, to be loaded when the server restarts
        FileIO.writeFile(filename,filetext,lambda: logger.debug('\t\tError saving the file:\r\r'))      


    #from planner.js
    #fs.readFile('./data/pipette_calibrations.json', 'utf8', function (err,data)
    #load_pipette_values()
    def load_pipette_values(self):
        """Load pipette values from data/pipette_calibrations.json
        """
        logger.debug('loading pipette calibrations:')
        old_values = FileIO.get_dict_from_json(os.path.join(self.dir_path,'otone_data/pipette_calibrations.json'))
        logger.debug(old_values)
        
        if self.PIPETTES is not None and len(self.PIPETTES) > 0:
            for axis in old_values:
                #for n in old_values[axis]:
                for k, v in old_values[axis].items():
                    self.PIPETTES[axis].__dict__[k] = v

                    # should include:
                    #  'resting'
                    #  'top'
                    #  'bottom'
                    #  'blowout'
                    #  'droptip'
                    #  'volume'
                    #  'theContainers'
            
        else:
            logger.debug('head.load_pipette_values: No pipettes defined in PIPETTES')
            
    #from planner.js
    # an array of new container names to be stored in each pipette
    #ToDo: this method may be redundant
    def create_deck(self, new_deck):
        """Create a dictionary of new container names to be stored in each pipette given a deck list

        Calls :meth:`head.save_pipette_values` right before returning dictionary

        :returns: container data for each axis
        :rtype: dictionary
        
        """
        
        #doesn't map to smoothieAPI
        nameArray = []  

        for containerName in new_deck :
            nameArray.append(containerName) 
        
        response = {}  
        
        for n in self.PIPETTES:
            response[n] = self.PIPETTES[n].create_deck(nameArray)  

        self.save_pipette_values() 
        return response         
            
            
    def get_deck(self):
        """Get a dictionary of container names currently stored in each pipette

        Calls :meth:`head.save_pipette_values` right before returning dictionary

        :returns: container data for each axis
        :rtype: dictionary
        
        """
        response = {}
        for axis in self.PIPETTES:
            response[axis] = {}
            for name in self.PIPETTES[axis].theContainers:
                response[axis][name] = self.PIPETTES[axis].theContainers[name]
  
        self.save_pipette_values()

        return response


    def get_pipettes(self):
        """Get a dictionary of pipette properties for each pipette on head

        :returns: Pipette properties for each pipette
        :rtype: dictionary
        
        """
        response = {}

        for axis in self.PIPETTES:
            response[axis] = {}
            for k, v in self.PIPETTES[axis].__dict__.items():
                response[axis][k] = v
            
            # should include:
            #  'top'
            #  'bottom'
            #  'blowout'
            #  'droptip'
            #  'volume'
        
        return response

    #from planner.js
    def move_pipette(self, axis, property_):
        """Move the pipette to one of it's calibrated positions (top, bottom, blowout, droptip)
        
        This command is useful for seeing saved pipette positions while calibrating
        """
        #doesn't map to smoothieAPI
        #function movePipette (axis, property)
        if self.PIPETTES[axis] and property_ in self.PIPETTES[axis].__dict__:
            moveCommand = {}
            moveCommand[axis] = self.PIPETTES[axis].__dict__[property_]
            self.move(moveCommand)
            
    
    
    def move_plunger(self, axis, locations):
        """Move the plunger for given axis according to locations

        :note: This is only called from :class:`subscriber` and may be redundant

        locations = [loc, loc, etc... ]

        loc = {'plunger' : number}

        """

        if(self.PIPETTES[axis]):
            for i in range(len(locations)):
                moveCommand = self.PIPETTES[axis].pmap(locations[i])
                self.move(moveCommand)


    def erase_job(self):
        """Tell theQueue to clear
        """
        self.smoothieAPI.delay_cancel()
        self.theQueue.clear()


    def publish_calibrations(self):
        """Publish calibrations data
        """
        self.pubber.send_message('containerLocations',self.get_deck())
        self.pubber.send_message('pipetteValues',self.get_pipettes())