class Motor(object): ''' Motor class contains the methods necessary to send commands to the motor controllers for the corner and drive motors. There are many other ways of commanding the motors from the RoboClaw, we suggest trying to write your own Closed loop feedback method for the drive motors! ''' def __init__(self,config): super(Motor,self).__init__(config) self.rc = Roboclaw( config['CONTROLLER_CONFIG']['device'], config['CONTROLLER_CONFIG']['baud_rate'] ) self.rc.Open() self.address = config['MOTOR_CONFIG']['controller_address'] self.accel = [0] * 10 self.qpps = [None] * 10 self.err = [None] * 5 version = 1 for address in self.address: version = version & self.rc.ReadVersion(address)[0] print(self.rc.ReadVersion(address)[0]) if version != 0: print("[Motor__init__] Sucessfully connected to RoboClaw motor controllers") else: print("-----") raise Exception("Unable to establish connection to Roboclaw motor controllers") self.enc_min =[] self.enc_max =[] for address in self.address: self.rc.SetMainVoltages(address, int(config['BATTERY_CONFIG']['low_voltage']*10), int(config['BATTERY_CONFIG']['high_voltage']*10) ) if address == 131 or address == 132: self.rc.SetM1MaxCurrent(address, int(config['MOTOR_CONFIG']['max_corner_current']*100)) self.rc.SetM2MaxCurrent(address, int(config['MOTOR_CONFIG']['max_corner_current']*100)) self.enc_min.append(self.rc.ReadM1PositionPID(address)[-2]) self.enc_min.append(self.rc.ReadM2PositionPID(address)[-2]) self.enc_max.append(self.rc.ReadM1PositionPID(address)[-1]) self.enc_max.append(self.rc.ReadM2PositionPID(address)[-1]) else: self.rc.SetM1MaxCurrent(address, int(config['MOTOR_CONFIG']['max_drive_current']*100)) self.rc.SetM2MaxCurrent(address, int(config['MOTOR_CONFIG']['max_drive_current']*100)) self.rc.ResetEncoders(address) for address in self.address: self.rc.WriteNVM(address) for address in self.address: self.rc.ReadNVM(address) voltage = self.rc.ReadMainBatteryVoltage(0x80)[1]/10.0 if voltage >= config['BATTERY_CONFIG']['low_voltage']: print("[Motor__init__] Voltage is safe at: ",voltage, "V") else: raise Exception("Unsafe Voltage of" + voltage + " Volts") i = 0 for address in self.address: self.qpps[i] = self.rc.ReadM1VelocityPID(address)[4] self.accel[i] = int(self.qpps[i]*2) self.qpps[i+1] = self.rc.ReadM2VelocityPID(address)[4] self.accel[i+1] = int(self.qpps[i]*2) i+=2 self.errorCheck() def cornerToPosition(self,tick): ''' Method to send position commands to the corner motor :param list tick: A list of ticks for each of the corner motors to move to, if tick[i] is 0 it instead stops that motor from moving ''' speed, accel = 1000,2000 #These values could potentially need tuning still self.errorCheck() for i in range(4): index = int(math.ceil((i+1)/2.0)+2) if tick[i]: if (i % 2): self.rc.SpeedAccelDeccelPositionM2(self.address[index],accel,speed,accel,tick[i],1) else: self.rc.SpeedAccelDeccelPositionM1(self.address[index],accel,speed,accel,tick[i],1) else: if not (i % 2): self.rc.ForwardM1(self.address[index],0) else: self.rc.ForwardM2(self.address[index],0) def sendMotorDuty(self, motorID, speed): ''' Wrapper method for an easier interface to control the drive motors, sends open-loop commands to the motors :param int motorID: number that corresponds to each physical motor :param int speed: Speed for each motor, range from 0-127 ''' self.errorCheck() addr = self.address[int(motorID/2)] if speed > 0: if not motorID % 2: command = self.rc.ForwardM1 else: command = self.rc.ForwardM2 else: if not motorID % 2: command = self.rc.BackwardM1 else: command = self.rc.BackwardM2 speed = abs(int(speed * 127)) return command(addr,speed) def killMotors(self): ''' Stops all motors on Rover ''' for i in range(5): self.rc.ForwardM1(self.address[i],0) self.rc.ForwardM2(self.address[i],0) def errorCheck(self): ''' Checks error status of each motor controller, returns 0 if any errors occur ''' for i in range(5): self.err[i] = self.rc.ReadError(self.address[i])[1] for error in self.err: if error: self.killMotors() self.writeError() raise Exception("Motor controller Error", error) return 1 def writeError(self): ''' Writes the list of errors to a text file for later examination ''' f = open('errorLog.txt','a') errors = ','.join(str(e) for e in self.err) f.write('\n' + 'Errors: ' + '[' + errors + ']' + ' at: ' + str(datetime.datetime.now())) f.close()
class Agitator(object): '''Class for controlling the EXPRES fiber agitator Inputs ------ comport : str The hardware COM Port for the Roboclaw motor controller Public Methods -------------- start(exp_time, timeout, rot1, rot2): Threaded agitation start_agitation(exp_time, rot1, rot2): Unthreaded agitation stop(): Stop either threaded or unthreaded agitation stop_agitation(): Hard-stop agitation but will not close thread ''' def __init__(self, comport=__DEFAULT_PORT__): self._rc = Roboclaw(comport=comport, rate=__DEFAULT_BAUD_RATE__, addr=__DEFAULT_ADDR__, timeout=__DEFAULT_TIMEOUT__, retries=__DEFAULT_RETRIES__, inter_byte_timeout=__DEFAULT_INTER_BYTE_TIMEOUT__) # Create a logger for the agitator self.logger = logging.getLogger('expres_agitator') self.logger.setLevel(logging.DEBUG) # Create file handler to log all messages try: fh = logging.handlers.TimedRotatingFileHandler( 'C:/Users/admin/agitator_logs/agitator.log', when='D', interval=1, utc=True, backupCount=5) except FileNotFoundError: fh = logging.handlers.TimedRotatingFileHandler('agitator.log', when='D', interval=1, utc=True, backupCount=5) fh.setLevel(logging.DEBUG) # Create console handler to log info messages ch = logging.StreamHandler() ch.setLevel(logging.INFO) # create formatter and add it to the handlers formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(formatter) ch.setFormatter(formatter) # add the handlers to the logger self.logger.addHandler(fh) self.logger.addHandler(ch) self.thread = None # In case stop() is called before a thread is created self.stop_event = Event() # Used for stopping threads self.stop_agitation() # Just to make sure def __del__(self): ''' When the object is deleted, make sure all threads are closed and agitator is stopped. Unfortunately, these actions cannot be logged because the logger closes by the time __del__ is called... ''' self.stop(verbose=False) self.stop_agitation(verbose=False) def threaded_agitation(self, exp_time, timeout, **kwargs): '''Threadable function allowing stop event''' self.logger.info( 'Starting agitator thread for {}s exposure with {}s timeout'. format(exp_time, timeout)) self.start_agitation(exp_time, **kwargs) t = 0 start_time = time.time() while not self.stop_event.is_set() and t < timeout: time.sleep(1) t = time.time() - start_time self.logger.info('{}/{}s for {}s exposure. I1: {}, I2: {}'.format( round(t, 1), timeout, exp_time, self.current1, self.current2)) self.stop_agitation() self.stop_event.clear() # Allow for future agitation events def start(self, exp_time=60.0, timeout=None, **kwargs): ''' Start a thread that starts agitation and stops if a stop event is called or if the timeout is reached ''' self.stop() # To close any previously opened threads if timeout is None: # Allow for some overlap time timeout = exp_time + 10.0 self.thread = Thread(target=self.threaded_agitation, args=(exp_time, timeout), kwargs=kwargs) self.thread.start() def stop(self, verbose=True): '''Stop the agitation thread if it is running''' if self.thread is not None and self.thread.is_alive(): while self.voltage1 > 0 or self.voltage2 > 0: if verbose: self.logger.info('Attempting to stop threaded agitation') self.stop_event.set() self.thread.join(2) if self.voltage1 > 0 or self.voltage2 > 0: # As a backup in case something went wrong if verbose: self.logger.error( 'Something went wrong when trying to stop threaded agitation. Forcing agitator to stop.' ) self.stop_agitation() def start_agitation(self, exp_time=60.0, rot=None): '''Set the motor voltages for the given number of rotations in exp_time''' if exp_time <= 0: self.logger.warning( 'Non-positive exposure time given to agitator object') self.stop_agitation() return if rot is None: rot = 0.5 * exp_time if rot <= 0: self.logger.warning( 'Non-positive rotation number given to agitator object') self.stop_agitation() return freq1 = rot / exp_time freq2 = 0.9 * rot / exp_time self._freq = freq1 self.logger.info('Starting agitation at approximately {} Hz'.format( self._freq)) self.set_voltage1(Motor1.calc_voltage(self.battery_voltage, freq1)) self.set_voltage2(Motor2.calc_voltage(self.battery_voltage, freq2)) def stop_agitation(self, verbose=True): '''Set both motor voltages to 0''' if verbose: self.logger.info('Stopping agitation') self.set_voltage(0) self._freq = 0 def set_voltage(self, voltage): '''Set both motor voltages to the given voltage''' self.set_voltage1(voltage) self.set_voltage2(voltage) # Getter for the frequency def get_freq(self): self.logger.info('Requesting frequency') return self._freq # Getters and setters for the motor voltages def get_voltage1(self): return self._voltage1 def set_voltage1(self, voltage): battery_voltage = self.battery_voltage if abs(voltage) > battery_voltage: voltage = np.sign(voltage) * battery_voltage if voltage >= 0: self._rc.ForwardM1(int(voltage / battery_voltage * 127)) else: self._rc.BackwardM1(int(-voltage / battery_voltage * 127)) self._voltage1 = voltage voltage1 = property(get_voltage1, set_voltage1) def get_voltage2(self): return self._voltage2 def set_voltage2(self, voltage): battery_voltage = self.battery_voltage if abs(voltage) > battery_voltage: voltage = np.sign(voltage) * battery_voltage if voltage >= 0: self._rc.ForwardM2(int(voltage / battery_voltage * 127)) else: self._rc.BackwardM2(int(-voltage / battery_voltage * 127)) self._voltage2 = voltage voltage2 = property(get_voltage2, set_voltage2) def get_max_voltage(self): return self._rc.ReadMinMaxMainVoltages()[2] / 10 def set_max_voltage(self, voltage): self._rc.SetMainVoltages(int(self.min_voltage * 10), int(voltage * 10)) max_voltage = property(get_max_voltage, set_max_voltage) def get_min_voltage(self): return self._rc.ReadMinMaxMainVoltages()[1] / 10 def set_min_voltage(self, voltage): self._rc.SetMainVoltages(int(voltage * 10), int(self.max_voltage * 10)) min_voltage = property(get_min_voltage, set_min_voltage) # Getter for the motor controller power source voltage def get_battery_voltage(self): return self._rc.ReadMainBatteryVoltage()[1] / 10 battery_voltage = property(get_battery_voltage) # Getters and setters for the motor currents def get_current1(self): return self._rc.ReadCurrents()[1] / 100 current1 = property(get_current1) def get_current2(self): return self._rc.ReadCurrents()[2] / 100 current2 = property(get_current2) def get_max_current1(self): return self._rc.ReadM1MaxCurrent()[1] / 100 def set_max_current1(self, current): self._rc.SetM1MaxCurrent(current) max_current1 = property(get_max_current1, set_max_current1) def get_max_current2(self): return self._rc.ReadM2MaxCurrent()[1] / 100 def set_max_current2(self, current): self._rc.SetM2MaxCurrent(current) max_current2 = property(get_max_current2, set_max_current2)
from roboclaw import Roboclaw rc = Roboclaw("/dev/ttyS3", 115200) rc.Open() rc.ReadVersion(0x80) # This is the old API, use the new one below. rc.SetMinVoltageMainBattery(0x80, 110) # 11 Volts rc.ReadMinMaxMainVoltages(0x80) rc.SetMainVoltages(0x80, 110, 340) # Allowed range: 11 V - 34 V rc.SetM1MaxCurrent(0x80, 500) # 5 Amps rc.SetPWMMode(0x80, 0) # Locked Antiphase #rc.ReadPWMMode(0x80) rc.SetM1EncoderMode(0x80, 0) # No RC/Analog support + Quadrature encoder #rc.ReadEncoderModes(0x80) getConfig = rc.GetConfig(0x80) config = getConfig[1] # index zero is 1 for success, 0 for failure. config = config | 0x0003 # Packet serial mode config = config | 0x8000 # Multi-Unit mode rc.SetConfig(0x80, config) rc.SetPinFunctions(0x80, 2, 0, 0) # S3 = E-Stop, S4 = Disabled, S5 = Disabled rc.WriteNVM(0x80) rc.ReadEncM1(0x80) rc.ResetEncoders(0x80) rc.ReadEncM1(0x80) p = 15000 i = 1000