From 534813ea96a06b103792a682f075535193163854 Mon Sep 17 00:00:00 2001 From: damic014 Date: Tue, 26 Nov 2019 01:46:09 -0600 Subject: Add encoder measurements via thread. This should help with linear encoder accuracy and SW interrupt accuracy. Coupled with changes from the interrupt enhancement branch, the limit behavior should be much improved. Added a parameter to allow user to set their own SW limit-reached routine (default behavior is still the same). This should also help prevent over-excursion in the swingup test. Slightly modified swingup test to use new SW limit-reached routine. This should help prevent the system from over-excursing when the soft limits of the program are reached. Theoretically the program shouldn't be able to hit the HW limits anymore. --- System/system.py | 40 ++++++++++++++++++++++++++++++++-------- System/system_swingup_test_2.py | 21 +++++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) (limited to 'System') diff --git a/System/system.py b/System/system.py index 75573c5..c06063a 100644 --- a/System/system.py +++ b/System/system.py @@ -7,6 +7,7 @@ from time import sleep import RPi.GPIO as GPIO import sys import os +from threading import Thread # IO pin definitions ### Motor pins @@ -31,7 +32,7 @@ system_min_x = -16.5 # System Class # This is the primary interface a student will use to control the pendulum. class System: - def __init__(self, negative_limit=float('nan'), positive_limit=float('nan'), angular_units='Degrees'): + def __init__(self, negative_limit=float('nan'), positive_limit=float('nan'), angular_units='Degrees', sw_limit_routine=self.limit_triggered): GPIO.setwarnings(False) # Initialize public variables self.max_x = system_max_x @@ -46,6 +47,12 @@ class System: # We assume that the system has been initialized on startup to a 0 position, or that the previous run ended by returning the system to 0 self.encoder_linear.set_zero() + # Setup a thread to constantly be measuring encoder positions + self.encoder_thread = Thread(target = self.encoder_thread_routine) + self.angular_position = 0. + self.linear_position = 0. + self.encoder_thread.start() + # Enable hardware interrupts for hardware limit switches GPIO.setup(limit_negative_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(limit_negative_pin, GPIO.FALLING, callback=self.negative_limit_callback) @@ -64,6 +71,7 @@ class System: self.positive_soft_limit = float('nan') # NOTE: If only one limit has been defined, this should always work (hardware limits will be the absolute edge on the undefined side, although this would be difficult for users to utilize unless we provide the position of the hardware limits on each side # NOTE: If neither limit is defined, the hardware limits will be the only limits in effect. + self.sw_limit_routine = sw_limit_routine self.angular_units = angular_units @@ -122,6 +130,7 @@ class System: def deinitialize(self): self.return_home() self.motor.brake() + self.encoder_thread.terminate() GPIO.cleanup() # Get the values of the encoders to determine the angular and linear position of the pendulum. @@ -134,28 +143,43 @@ class System: ##### Positive values indicate the pendulum is right-of-center. ##### Negative values indicate the pendulum is left-of-center. def measure(self): + return (self.angular_position, self.linear_position) + # END measure() + + # Thread routine (0.1s interval). Get the values of the encoders to determine the angular and linear position of the pendulum. + # Values are saved in the class (self.angular_position and self.linear_position), which are then simply returned by measure() + def encoder_thread_routine(self): angular_position = self.encoder_angular.read_position(self.angular_units) if self.angular_units == 'Degrees': if angular_position > 180.: angular_position = angular_position - 360. - linear_position = self.encoder_linear.read_position() + self.angular_position = angular_position + self.linear_position = self.encoder_linear.read_position() # Check soft limits - if (not math.isnan(self.negative_soft_limit) and linear_position < self.negative_soft_limit) or linear_position < self.min_x: + if (not math.isnan(self.negative_soft_limit) and self.linear_position < self.negative_soft_limit) or self.linear_position < self.min_x: + # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced + #self.interrupted = True + self.motor.brake() # Print negative soft limit violation to the results file. result_file = open(self.result_filename, "a") result_file.write("Negative software limit %f has been reached!" % self.negative_soft_limit) result_file.close() # Fire the limit trigger method (stops motor, kills program immediately). - self.limit_triggered() - if (not math.isnan(self.positive_soft_limit) and linear_position > self.positive_soft_limit) or linear_position > self.max_x: + self.sw_limit_routine() + if (not math.isnan(self.positive_soft_limit) and self.linear_position > self.positive_soft_limit) or self.linear_position > self.max_x: + # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced + #self.interrupted = True + self.motor.brake() # Print positive soft limit violation to the results file. result_file = open(self.result_filename, "a") result_file.write("Positive software limit %f has been reached!" % self.positive_soft_limit) result_file.close() # Fire the limit trigger method (stops motor, kills program immediately). - self.limit_triggered() - return (angular_position, linear_position) - # END measure() + self.sw_limit_routine() + # If this point is reached, the SW limit routine was user-defined and did not kill the program - un-set the 'interrupted' flag + #self.interrupted = False + # This thread should run on ~0.1s intervals + sleep(0.1) # Adjust the pendulum's linear position using the motor. ### speed: Acceptable values range from -100 to 100 (as a percentage), with 100/-100 being the maximum adjustment speed. diff --git a/System/system_swingup_test_2.py b/System/system_swingup_test_2.py index 81d5419..e240b01 100644 --- a/System/system_swingup_test_2.py +++ b/System/system_swingup_test_2.py @@ -53,7 +53,7 @@ class SwingUpEnv(): } def __init__(self): - self.sys = System(angular_units='Radians') + self.sys = System(angular_units='Radians', positive_limit=10., negative_limit=-10., sw_limit_routine=self.x_threshold_routine) self.force_mag = 10. self.last_time = time.time() # time for seconds between updates @@ -73,6 +73,7 @@ class SwingUpEnv(): self.state = None self.steps_beyond_done = None + self.done = False def seed(self, seed=None): self.np_random, seed = seeding.np_random(seed) @@ -83,7 +84,9 @@ class SwingUpEnv(): state = self.state x, x_dot, theta, theta_dot = state force = self.force_mag * action[0] - self.sys.adjust(force) + # Do not adjust the motor further if the x_threshold has been triggered by the SW limit + if self.done == False: + self.sys.adjust(force) costheta = math.cos(theta) sintheta = math.sin(theta) @@ -105,10 +108,15 @@ class SwingUpEnv(): self.state = (new_x, x_dot, new_theta, theta_dot) self.sys.add_results(new_theta, new_x, force) - done = x < -self.x_threshold \ + done = theta_dot < -self.theta_dot_threshold \ + or theta_dot > self.theta_dot_threshold \ + or self.done == True + + '''done = x < -self.x_threshold \ or x > self.x_threshold \ or theta_dot < -self.theta_dot_threshold \ - or theta_dot > self.theta_dot_threshold + or theta_dot > self.theta_dot_threshold \ + or self.done == True''' done = bool(done) if not done: @@ -125,6 +133,10 @@ class SwingUpEnv(): return np.array(self.state), reward, done, {'max_up_time' : self.max_up_time} + def x_threshold_routine(self): + self.done = True + self.sys.adjust(0) + def reset(self, home = True): if home == True: self.sys.return_home() @@ -138,6 +150,7 @@ class SwingUpEnv(): self.max_up_time = 0 self.up = False self.steps_beyond_done = None + self.done = False return np.array(self.state) def end(self): -- cgit v1.2.3 From 801718e1f0933c083e25f8389d2d0c8f803c45a7 Mon Sep 17 00:00:00 2001 From: damic014 Date: Tue, 26 Nov 2019 01:51:01 -0600 Subject: Modify test_System to verify that threaded encoder measurements are working as expected. Run this to check before actually running the system. --- System/test_System.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'System') diff --git a/System/test_System.py b/System/test_System.py index 21ed336..c05affc 100644 --- a/System/test_System.py +++ b/System/test_System.py @@ -2,20 +2,12 @@ # DO NOT TEST ON ASSEMBLED PHYSICAL SYSTEM! It will probably break it. import time -from system import System - -# Return a speed based on current encoder angle. -# Convert an angle to speed (180 degrees = max speed) -def control_function(angle): - return (abs(angle) / 180.0) * 100.0 +from System.system import System # Main program sys = System(angular_units = 'Radians') while 1: angle, linear = sys.measure() - print(angle) - """speed = control_function(angle) - print(speed) - sys.adjust(speed)""" - time.sleep(0.05) + print("Angle: " + str(angle) + ", Linear: " + str(linear)) + time.sleep(0.2) -- cgit v1.2.3 From 83823aebece092a845bf1b9eae44175f91f7115d Mon Sep 17 00:00:00 2001 From: Raspberry Pi Date: Tue, 26 Nov 2019 19:31:10 -0600 Subject: Got threading and SW limits working really well. Still might be some shakiness on deinitialization (need to coordinate GPIO.cleanup with threads), but program does exit and return home normally. Swingup test now runs very smooth and SW limits were increased with the drastically improved response time. Pushing to merge back to master. --- System/system.py | 122 ++++++++++++++++++++++------------------ System/system_swingup_test_2.py | 15 +++-- 2 files changed, 77 insertions(+), 60 deletions(-) (limited to 'System') diff --git a/System/system.py b/System/system.py index c06063a..63d0ecc 100644 --- a/System/system.py +++ b/System/system.py @@ -7,7 +7,7 @@ from time import sleep import RPi.GPIO as GPIO import sys import os -from threading import Thread +from threading import Thread, Lock # IO pin definitions ### Motor pins @@ -32,8 +32,9 @@ system_min_x = -16.5 # System Class # This is the primary interface a student will use to control the pendulum. class System: - def __init__(self, negative_limit=float('nan'), positive_limit=float('nan'), angular_units='Degrees', sw_limit_routine=self.limit_triggered): + def __init__(self, negative_limit=float('nan'), positive_limit=float('nan'), angular_units='Degrees', sw_limit_routine=None): GPIO.setwarnings(False) + self.deinit = False # Initialize public variables self.max_x = system_max_x self.min_x = system_min_x @@ -47,11 +48,7 @@ class System: # We assume that the system has been initialized on startup to a 0 position, or that the previous run ended by returning the system to 0 self.encoder_linear.set_zero() - # Setup a thread to constantly be measuring encoder positions - self.encoder_thread = Thread(target = self.encoder_thread_routine) - self.angular_position = 0. - self.linear_position = 0. - self.encoder_thread.start() + self.angular_units = angular_units # Enable hardware interrupts for hardware limit switches GPIO.setup(limit_negative_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) @@ -71,9 +68,9 @@ class System: self.positive_soft_limit = float('nan') # NOTE: If only one limit has been defined, this should always work (hardware limits will be the absolute edge on the undefined side, although this would be difficult for users to utilize unless we provide the position of the hardware limits on each side # NOTE: If neither limit is defined, the hardware limits will be the only limits in effect. - self.sw_limit_routine = sw_limit_routine - - self.angular_units = angular_units + self.sw_limit_routine = self.limit_triggered + if sw_limit_routine is not None: + self.sw_limit_routine = sw_limit_routine # Create and setup results file (to be sent back to the server and displayed/downloaded to the user) # Results file is a CSV with the following entries: angle, position, speed @@ -82,6 +79,14 @@ class System: result_file = open(self.result_filename, "w+") result_file.write("angle(" + angular_units + "),position(inches),speed(percentage)\n") result_file.close() + + # Setup a thread to constantly be measuring encoder positions + #self.encoder_thread = EncoderThread(instance = self) + self.encoder_thread = Thread(target = self.encoder_thread_routine) + self.encoder_thread.setDaemon(True) + self.angular_position = 0. + self.linear_position = 0. + self.encoder_thread.start() # END __init__() def initialize(self): @@ -90,7 +95,7 @@ class System: GPIO.remove_event_detect(limit_positive_pin) # Begin moving slowly in the negative direction until the negative limit switch is triggered if not GPIO.input(limit_negative_pin) == False: - self.motor.move(-4) + self.motor.move(-5) pressed = True while pressed != False: pressed = GPIO.input(limit_negative_pin) @@ -100,24 +105,22 @@ class System: self.encoder_linear.set_zero() sleep(1) # Begin moving slowly in the positive direction until the positive limit switch is triggered - self.motor.move(4) + self.motor.move(5) pressed = True while pressed != False: # We must continue reading linear encoder motion to keep track of rotations - position = self.encoder_linear.read_position() pressed = GPIO.input(limit_positive_pin) sleep(0.01) - #GPIO.wait_for_edge(limit_positive_pin, GPIO.FALLING) self.motor.brake() # Get the current position (the extent of the track) - extent = self.encoder_linear.read_position() + extent = self.linear_position # Move back towards the center until we reach position extent/2 position = extent sleep(1) self.motor.move(-4) while position >= (extent / 2.): - position = self.encoder_linear.read_position() - sleep(0.01) + position = self.linear_position + sleep(0.015) self.motor.brake() # Set zero again: this is the real zero self.encoder_linear.set_zero() @@ -130,7 +133,10 @@ class System: def deinitialize(self): self.return_home() self.motor.brake() - self.encoder_thread.terminate() + self.deinit = True + if self.encoder_thread.isAlive(): + self.encoder_thread.join() + sleep(1) GPIO.cleanup() # Get the values of the encoders to determine the angular and linear position of the pendulum. @@ -146,40 +152,48 @@ class System: return (self.angular_position, self.linear_position) # END measure() + # Thread routine (0.1s interval). Get the values of the encoders to determine the angular and linear position of the pendulum. # Values are saved in the class (self.angular_position and self.linear_position), which are then simply returned by measure() def encoder_thread_routine(self): - angular_position = self.encoder_angular.read_position(self.angular_units) - if self.angular_units == 'Degrees': - if angular_position > 180.: - angular_position = angular_position - 360. - self.angular_position = angular_position - self.linear_position = self.encoder_linear.read_position() - # Check soft limits - if (not math.isnan(self.negative_soft_limit) and self.linear_position < self.negative_soft_limit) or self.linear_position < self.min_x: - # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced - #self.interrupted = True - self.motor.brake() - # Print negative soft limit violation to the results file. - result_file = open(self.result_filename, "a") - result_file.write("Negative software limit %f has been reached!" % self.negative_soft_limit) - result_file.close() - # Fire the limit trigger method (stops motor, kills program immediately). - self.sw_limit_routine() - if (not math.isnan(self.positive_soft_limit) and self.linear_position > self.positive_soft_limit) or self.linear_position > self.max_x: - # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced - #self.interrupted = True - self.motor.brake() - # Print positive soft limit violation to the results file. - result_file = open(self.result_filename, "a") - result_file.write("Positive software limit %f has been reached!" % self.positive_soft_limit) - result_file.close() - # Fire the limit trigger method (stops motor, kills program immediately). - self.sw_limit_routine() - # If this point is reached, the SW limit routine was user-defined and did not kill the program - un-set the 'interrupted' flag - #self.interrupted = False - # This thread should run on ~0.1s intervals - sleep(0.1) + limit_serviced = False + while self.deinit == False: + angular_position = self.encoder_angular.read_position(self.angular_units) + if self.angular_units == 'Degrees': + if angular_position > 180.: + angular_position = angular_position - 360. + self.angular_position = angular_position + self.linear_position = self.encoder_linear.read_position() + # Check soft limits + if (not math.isnan(self.negative_soft_limit)) and self.linear_position < self.negative_soft_limit: #or self.linear_position < self.min_x: + if limit_serviced == False: + limit_serviced = True + # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced + #self.interrupted = True + self.motor.brake() + # Print negative soft limit violation to the results file. + result_file = open(self.result_filename, "a") + result_file.write("Negative software limit %f has been reached!" % self.negative_soft_limit) + result_file.close() + # Fire the limit trigger method + self.sw_limit_routine() + elif (not math.isnan(self.positive_soft_limit)) and self.linear_position > self.positive_soft_limit: #or self.linear_position > self.max_x: + if limit_serviced == False: + limit_serviced = True + # SW limit reached: stop the motor, set the interrupted flag so that the motor cannot continue to move until the interrupt has been completely serviced + #self.interrupted = True + self.motor.brake() + # Print positive soft limit violation to the results file. + result_file = open(self.result_filename, "a") + result_file.write("Positive software limit %f has been reached!" % self.positive_soft_limit) + result_file.close() + # Fire the limit trigger method + self.sw_limit_routine() + elif limit_serviced == True and self.linear_position > (self.negative_soft_limit+0.5) and self.linear_position < (self.positive_soft_limit-0.5): + # Clear the limit service flag once we return to a reasonable range that the limit will not trigger again + limit_serviced = False + # This thread should run on ~0.01s intervals + sleep(0.01) # Adjust the pendulum's linear position using the motor. ### speed: Acceptable values range from -100 to 100 (as a percentage), with 100/-100 being the maximum adjustment speed. @@ -215,20 +229,20 @@ class System: # Go back to the zero position (linear) so that the next execution starts in the correct place. def return_home(self): - position = self.encoder_linear.read_position() + position = self.linear_position # slowly move towards 0 until we get there if position > 0: self.motor.move(-4) while position > 0: - position = self.encoder_linear.read_position() - sleep(0.01) + position = self.linear_position + sleep(0.015) self.motor.brake() return else: self.motor.move(4) while position < 0: - position = self.encoder_linear.read_position() - sleep(0.01) + position = self.linear_position + sleep(0.015) self.motor.brake() return # END return_home diff --git a/System/system_swingup_test_2.py b/System/system_swingup_test_2.py index e240b01..5c0ac5a 100644 --- a/System/system_swingup_test_2.py +++ b/System/system_swingup_test_2.py @@ -53,13 +53,13 @@ class SwingUpEnv(): } def __init__(self): - self.sys = System(angular_units='Radians', positive_limit=10., negative_limit=-10., sw_limit_routine=self.x_threshold_routine) + self.x_threshold = 14. + self.sys = System(angular_units='Radians', positive_limit=self.x_threshold, negative_limit=-self.x_threshold, sw_limit_routine=self.x_threshold_routine) - self.force_mag = 10. + self.force_mag = 11. self.last_time = time.time() # time for seconds between updates # Angle at which to fail the episode - self.x_threshold = 10. self.x_dot_threshold = 10. self.theta_dot_threshold = 3*np.pi @@ -134,6 +134,7 @@ class SwingUpEnv(): return np.array(self.state), reward, done, {'max_up_time' : self.max_up_time} def x_threshold_routine(self): + print("SW Limit reached!") self.done = True self.sys.adjust(0) @@ -311,14 +312,16 @@ class sarsaAgent: env = SwingUpEnv() # For simplicity, we only consider forces of -1 and 1 -numActions = 5 +numActions = 2 Actions = np.linspace(-1, 1, numActions) # This is our learning agent gamma = .95 -agent = sarsaAgent(5, numActions, 20, 1, epsilon = 5e-2, gamma = gamma, alpha = 1e-5) +#agent = sarsaAgent(5, numActions, 20, 1, epsilon = 5e-2, gamma = gamma, alpha = 1e-5) +agent = deepQagent(5,numActions,20,2,epsilon=5e-2,gamma=gamma,batch_size=20, + c= 100,alpha=1e-4) -maxSteps = 5e4 +maxSteps = 2e5 # This is a helper to deal with the fact that x[2] is actually an angle x_to_y = lambda x : np.array([x[0], x[1], np.cos(x[2]), np.sin(x[2]), x[3]]) -- cgit v1.2.3