From 17224a0b2bf9cec9693a84aa59987464d2e97086 Mon Sep 17 00:00:00 2001 From: rsteinberg83 <75146364+rsteinberg83@users.noreply.github.com> Date: Wed, 2 Feb 2022 09:34:32 -0800 Subject: [PATCH] Auger Support added --- main.py | 273 ++++++++++++++++++++++++++++++++++++++++ motors.py | 145 +++++++++++++++++++++ opentrickler_config.ini | 76 +++++++++++ 3 files changed, 494 insertions(+) create mode 100644 main.py create mode 100644 motors.py create mode 100644 opentrickler_config.ini diff --git a/main.py b/main.py new file mode 100644 index 0000000..ce5b523 --- /dev/null +++ b/main.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import datetime +import decimal +import logging +import time + +import constants +import helpers +import PID +import motors +import scales + + +# Components: +# 0. Server (Pi) +# 1. Scale (serial) +# 2. Trickler (gpio/PWM) +# 3. Dump (gpio/servo?) +# 4. API +# 6. Bluetooth? +# 7: Powder pan/cup? + +# TODO +# - document specific python version +# - handle case where scale is booted with pan on -- shows error instead of negative value +# - detect scale that's turned off (blank values) +# - validate inputs (target weight) + + +def auger_loop(memcache, auger_motor, scale, target_weight, target_unit, auger_dir, auger_speed, auger_steps): + logging.info('Starting auger motor...') + + while 1: + # Stop running if auto mode is disabled. + if not memcache.get(constants.AUTO_MODE): + logging.debug('auto mode disabled.') + break + + # Read scale values (weight/unit/stable) + scale.update() + + # Stop running if scale's unit no longer matches target unit. + if scale.unit != target_unit: + logging.debug('Target unit does not match scale unit.') + break + + # Stop running if pan removed. + if scale.weight < 0: + logging.debug('Pan removed.') + break + + # Spin the Auger as defined by init file + auger_motor.moveSteps(memcache, auger_dir, auger_speed, auger_steps) + + # Read scale values (weight/unit/stable) + scale.update() + + remainder_weight = target_weight - scale.weight + logging.debug('remainder_weight: %r', remainder_weight) + + + # Augering complete. + if remainder_weight <= 0: + logging.debug('Augering complete, motor turned off.') + break + + + # Clean up tasks. + auger_motor.motorStop() + logging.info('Augering process stopped.') + + +def trickler_loop(memcache, pid, trickler_motor, scale, target_weight, target_unit, pidtune_logger): + pidtune_logger.info('timestamp, input (motor %), output (weight %)') + logging.info('Starting trickling process...') + + while 1: + # Stop running if auto mode is disabled. + if not memcache.get(constants.AUTO_MODE): + logging.debug('auto mode disabled.') + break + + # Read scale values (weight/unit/stable) + scale.update() + + # Stop running if scale's unit no longer matches target unit. + if scale.unit != target_unit: + logging.debug('Target unit does not match scale unit.') + break + + # Stop running if pan removed. + if scale.weight < 0: + logging.debug('Pan removed.') + break + + remainder_weight = target_weight - scale.weight + logging.debug('remainder_weight: %r', remainder_weight) + + pidtune_logger.info( + '%s, %s, %s', + datetime.datetime.now().timestamp(), + trickler_motor.speed, + scale.weight / target_weight) + + # Trickling complete. + if remainder_weight <= 0: + logging.debug('Trickling complete, motor turned off and PID reset.') + break + + # PID controller requires float value instead of decimal.Decimal + pid.update(float(scale.weight / target_weight) * 100) + trickler_motor.update(pid.output) + logging.debug('trickler_motor.speed: %r, pid.output: %r', trickler_motor.speed, pid.output) + logging.info( + 'remainder: %s %s scale: %s %s motor: %s', + remainder_weight, + target_unit, + scale.weight, + scale.unit, + trickler_motor.speed) + + # Clean up tasks. + trickler_motor.off() + # Clear PID values. + pid.clear() + logging.info('Trickling process stopped.') + + +def main(config, args, pidtune_logger): + memcache = helpers.get_mc_client() + + pid = PID.PID( + float(config['PID']['Kp']), + float(config['PID']['Ki']), + float(config['PID']['Kd'])) + logging.debug('pid: %r', pid) + + trickler_motor = motors.TricklerMotor( + memcache=memcache, + motor_pin=int(config['motors']['trickler_pin']), + min_pwm=int(config['motors']['trickler_min_pwm']), + max_pwm=int(config['motors']['trickler_max_pwm'])) + logging.debug('trickler_motor: %r', trickler_motor) + + auger_in1=int(config['motors']['auger_in1']) + auger_in2=int(config['motors']['auger_in2']) + auger_in3=int(config['motors']['auger_in3']) + auger_in4=int(config['motors']['auger_in4']) + auger_dir=int(config['motors']['auger_dir']) + auger_speed=int(config['motors']['auger_speed']) + auger_steps=int(config['motors']['auger_steps']) + auger_lpct=int(config['motors']['auger_lpct']) + auger_rsteps=int(config['motors']['auger_rsteps']) + auger_wait=int(config['motors']['auger_wait']) + + auger_motor = motors.AugerMotor( + memcache=memcache, + auger_in1=auger_in1, + auger_in2=auger_in2, + auger_in3=auger_in3, + auger_in4=auger_in4, + auger_dir=auger_dir) + + if auger_dir == 0: + auger_rdir = 1 + else: + auger_rdir = 0 + + logging.debug('auger motor driver pins [in1 - in4]: %r %r %r %r', auger_in1,auger_in2,auger_in3,auger_in4,) + logging.debug('auger motor direction, reverse direction speed, step, load percent: %r %r %r %r %r', auger_dir, auger_rdir, auger_speed, auger_steps, auger_lpct) + + #servo_motor = gpiozero.AngularServo(int(config['motors']['servo_pin'])) + + scale = scales.SCALES[config['scale']['model']]( + memcache=memcache, + port=config['scale']['port'], + baudrate=int(config['scale']['baudrate']), + timeout=float(config['scale']['timeout'])) + logging.debug('scale: %r', scale) + + memcache.set(constants.AUTO_MODE, args.auto_mode or False) + memcache.set(constants.TARGET_WEIGHT, args.target_weight or decimal.Decimal('0.0')) + memcache.set(constants.TARGET_UNIT, scales.UNIT_MAP.get(args.target_unit, 'GN')) + + while 1: + # Update settings. + auto_mode = memcache.get(constants.AUTO_MODE) + target_weight = memcache.get(constants.TARGET_WEIGHT) + target_unit = memcache.get(constants.TARGET_UNIT) + # Use percentages for PID control to avoid complexity w/ different units of weight. + pid.SetPoint = 100.0 + scale.update() + + # Set scale to match target unit. + if target_unit != scale.unit: + logging.info('scale.unit: %r, target_unit: %r', scale.unit, target_unit) + scale.change_unit() + + logging.info( + 'target: %s %s scale: %s %s auto_mode: %s', + target_weight, + target_unit, + scale.weight, + scale.unit, + auto_mode) + + # Powder pan in place, scale stable, ready to trickle. + if (scale.weight >= 0 and + scale.weight < target_weight and + scale.unit == target_unit and + scale.is_stable and + auto_mode): + # Wait a second to start trickling. + time.sleep(1) + + # If reverse cycle is wanted... + if auger_rsteps > 0 : + auger_motor.moveSteps(memcache, auger_rdir, auger_speed, auger_rsteps) + time.sleep(.250) + auger_motor.motorStop() + time.sleep(.250) + + # Run Auger loop to auger_lpct + atarget_weight = int(float(target_weight) * (0.01 * float(auger_lpct)) ) + logging.debug('Starting auger process - target weight: %r', atarget_weight) + auger_loop(memcache, auger_motor, scale, atarget_weight, target_unit, auger_dir, auger_speed, auger_steps) + + # Wait a bit + time.sleep(auger_wait *.001) + + # Run trickler loop. + trickler_loop(memcache, pid, trickler_motor, scale, target_weight, target_unit, pidtune_logger) + + +if __name__ == '__main__': + import argparse + import configparser + + parser = argparse.ArgumentParser(description='Run OpenTrickler.') + parser.add_argument('config_file') + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--auto_mode', action='store_true') + parser.add_argument('--pid_tune', action='store_true') + parser.add_argument('--target_weight', type=decimal.Decimal, default=0) + parser.add_argument('--target_unit', choices=scales.UNIT_MAP.keys(), default='GN') + args = parser.parse_args() + + config = configparser.ConfigParser() + config.read_file(open(args.config_file)) + + log_level = logging.INFO + if args.verbose or config['general'].getboolean('verbose'): + log_level = logging.DEBUG + + helpers.setup_logging(log_level) + + pidtune_logger = logging.getLogger('pid_tune') + pid_handler = logging.StreamHandler() + pid_handler.setFormatter(logging.Formatter('%(message)s')) + + pidtune_logger.setLevel(logging.ERROR) + if args.pid_tune or config['PID'].getboolean('pid_tuner_mode'): + pidtune_logger.setLevel(logging.INFO) + + main(config, args, pidtune_logger) diff --git a/motors.py b/motors.py new file mode 100644 index 0000000..9340716 --- /dev/null +++ b/motors.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import atexit +import logging + +import RPi.GPIO as GPIO +import gpiozero # pylint: disable=import-error; + +import time +import constants + + + +class AugerMotor: + """Controls a stepper motor on the Pi.""" + + + def __init__(self, memcache, auger_in1 = 22, auger_in2 = 23, auger_in3 = 24, auger_in4 = 25, auger_dir = 0): + """Constructor.""" + self._memcache = memcache + logging.debug ('Init Auger Motor') + #GPIO.setmode(GPIO.BOARD) # Numbers GPIOs by physical location + self.motorPins = (auger_in1,auger_in2,auger_in3,auger_in4) + for pin in self.motorPins: + GPIO.setup(pin,GPIO.OUT) + self.auger_dir = auger_dir + atexit.register(self._graceful_exit) + + def _graceful_exit(self): + """Graceful exit function, turn off motor and close GPIO pin.""" + logging.debug('Closing AUGER motor...') + + def moveOnePeriod(self, direction,ms): + self.CCWStep = (0x01,0x02,0x04,0x08) + self.CWStep = (0x08,0x04,0x02,0x01) + for j in range(0,4,1): #cycle for power supply order + for i in range(0,4,1): #assign to each pin, a total of 4 pins + if (direction == 1):#power supply order clockwise + GPIO.output(self.motorPins[i],((self.CCWStep[j] == 1<<i) and GPIO.HIGH or GPIO.LOW)) + else : #power supply order anticlockwise + GPIO.output(self.motorPins[i],((self.CWStep[j] == 1<<i) and GPIO.HIGH or GPIO.LOW)) + if(ms<0): #the delay can not be less than 3ms, otherwise it will exceed speed limit of the motor + ms = 0 + time.sleep(ms*0.001) + + def moveSteps(self, memcache, direction, ms, steps): + for i in range(steps): + # Stop running if auto mode is disabled. + if not memcache.get(constants.AUTO_MODE): + logging.debug('auto mode disabled.') + break + self.moveOnePeriod(direction, ms) + + def motorStop(self): + for i in range(0,4,1): + GPIO.output(self.motorPins[i],GPIO.LOW) + + +class TricklerMotor: + """Controls a small vibration DC motor with the PWM controller on the Pi.""" + + def __init__(self, memcache, motor_pin=18, min_pwm=15, max_pwm=100): + """Constructor.""" + self._memcache = memcache + self.pwm = gpiozero.PWMOutputDevice(motor_pin) + self.min_pwm = min_pwm + self.max_pwm = max_pwm + logging.debug( + 'Created pwm motor on PIN %r with min %r and max %r: %r', + motor_pin, + self.min_pwm, + self.max_pwm, + self.pwm) + atexit.register(self._graceful_exit) + + def _graceful_exit(self): + """Graceful exit function, turn off motor and close GPIO pin.""" + logging.debug('Closing trickler motor...') + self.pwm.off() + self.pwm.close() + + def update(self, target_pwm): + """Change PWM speed of motor (int), enforcing clamps.""" + logging.debug('Updating target_pwm to %r', target_pwm) + target_pwm = max(min(int(target_pwm), self.max_pwm), self.min_pwm) + logging.debug('Adjusted clamped target_pwm to %r', target_pwm) + self.set_speed(target_pwm / 100) + + def set_speed(self, speed): + """Sets the PWM speed (float) and circumvents any clamps.""" + # TODO(eric): must be 0 - 1. + logging.debug('Setting speed from %r to %r', self.speed, speed) + self.pwm.value = speed + self._memcache.set(constants.TRICKLER_MOTOR_SPEED, self.speed) + + def off(self): + """Turns motor off.""" + self.set_speed(0) + + @property + def speed(self): + """Returns motor speed (float).""" + return self.pwm.value + + + +if __name__ == '__main__': + import argparse + import time + + import helpers + + parser = argparse.ArgumentParser(description='Test motors.') + parser.add_argument('--trickler_motor_pin', type=int, default=18) + #parser.add_argument('--servo_motor_pin', type=int) + parser.add_argument('--max_pwm', type=float, default=100) + parser.add_argument('--min_pwm', type=float, default=15) + args = parser.parse_args() + + memcache_client = helpers.get_mc_client() + + helpers.setup_logging() + + motor = TricklerMotor( + memcache=memcache_client, + motor_pin=args.trickler_motor_pin, + min_pwm=args.min_pwm, + max_pwm=args.max_pwm) + print('Spinning up trickler motor in 3 seconds...') + time.sleep(3) + for x in range(1, 101): + motor.set_speed(x / 100) + time.sleep(.05) + for x in range(100, 0, -1): + motor.set_speed(x / 100) + time.sleep(.05) + motor.off() + print('Done.') diff --git a/opentrickler_config.ini b/opentrickler_config.ini new file mode 100644 index 0000000..209a8d2 --- /dev/null +++ b/opentrickler_config.ini @@ -0,0 +1,76 @@ +[general] +verbose = True + + +[bluetooth] +name = Trickler + + +[scale] +# See the SCALES variable in scales.py for options. +model = and-fx120 +port = /dev/ttyUSB0 +baudrate = 19200 +timeout = 0.1 + + +[motors] +trickler_pin = 18 +trickler_max_pwm = 100 +trickler_min_pwm = 32 + +# Auger Motor Drive Pins +auger_in1 = 22 +auger_in2 = 23 +auger_in3 = 24 +auger_in4 = 25 + +# Auger Direction 0 = CCW, 1 = CW +auger_dir = 0 +# Auger Speed - mSec between steps, min = 3 / the higher the slower the motor spins +auger_speed = 3 +# Auger Step - The number of steps at drive intervals, 512 steps = One rotation +auger_steps = 32 +# Auger Load Percent +auger_lpct = 80 +# Auger Reverse Steps - Number of steps to drive in reverse before going forward with auger +auger_rsteps = 10 +# Auger Wait = Wait time between auger stop and tricker stop +auger_wait = 2000 + +#servo_pin = + +[leds] +# https://gpiozero.readthedocs.io/en/stable/recipes_advanced.html#controlling-the-pi-s-own-leds +status_led_pin = 47 +active_high = False +enable_status_leds = True +ready_status_led_mode = pulse +running_status_led_mode = on +done_status_led_mode = fast_blink + + +[PID] +# Higher Kp values will: +# - decrease rise time +# - increase overshoot +# - slightly increase settling time +# - decrease steady-state error +# - degrade stability +Kp = 10 +# Higher Ki values will: +# - slightly decrease rise time +# - increase overshoot +# - increase settling time +# - largely decrease steady-state error +# - degrade stability +Ki = 2.3 +# Higher Kd values will: +# - slightly decrease rise time +# - decrease overshoot +# - decrease settling time +# - minorly affect steady-state error +# - improve stability +Kd = 3.75 +# Enable for use with pidtuner.com +pid_tuner_mode = False