"""The robot settings window is opened as a child of the
ReachMaster root application. It allows the user to set any
parameters required by the robot microcontroller (e.g.,
position smoothing, valve periods etc.), or having to do
with the robot kinematics. It provides options for the user
to select previously generated calibration and position
command files. Soon it will allow users to run a robot
calibration routine from within the ReachMaster application.
Todo:
* Add labels to specify the microcontroller/kinematics parameters
* Run calibration callback
* Re-add load calibration callback?
* Automate unit tests
"""
from .. import config
import tkinter as tk
import tkinter.filedialog
import tkinter.messagebox
import numpy as np
import os
[docs]class RobotSettings(tk.Toplevel):
"""The primary class for the robot settings window.
Configures and provides window callback methods. Also provides
methods to select robot calibration files, position command
files, as well as methods used to sample positions from the
command file. The `on_quit()` method is called prior to
application destruction.
Attributes
----------
config : dict
The current configuration settings for the application
pos_smoothing : instance
Tkinter StringVar that captures the user-selected position
smoothing parameter (0-1, 0 less smoothing).
tol : instance
Tkinter StringVar that captures the user-selected tolerance
(bits) for accepting robot convergence to the command
position. (i.e., the L2-norm of the potentiometer readings
must be less than tol).
period : instance
Tkinter StringVar that captures the user-selected valve
period (us). Equivalent to pulse width modulation (PWM)
period.
off_dur : instance
Tkinter StringVar that captures the user-selected minimum
off duration (ms) that is enforced between convergence
times. Once the robot converges to a command position, it
will not move to a new command position for this duration.
num_tol : instance
Tkinter StringVar that captures the user-selected number of
consecutive periods the robot must be within convergence in
order to be considered "in position".
(x/y/z)_(push/pull)_wt : instance
Tkinter StringVar that captures the user-selected weighting
coefficient placed on the `x/y/z` actuator in the
`push/pull` direction. Acts as a gain on the calibration
coefficients. Typically defaults to 1 for a good
calibration, but gives the user an option to make quick
manual adjustments without runnning an entirely new
calibration. Not used very often in practice.
rew_zone_x : instance
Tkinter StringVar that captures the user-selected minimum
`x` position (bits) the handle must cross in order to enter
the reward zone.
rew_zone_(y/z)_(min/max) : instance
Tkinter StringVar that captures the user-selected min/max
`x/z` positions (bits) the handle must be within in order to
be located in the reward zone. Conditions must be satisfied
on all actuators simultaneously.
calibration_file : instance
Tkinter StringVar that captures the user-selected robot
calibration file.
command_file : instance
Tkinter StringVar that captures the user-selected position
command file.
command_type : instance
Tkinter StringVar that captures the user-selected option
determining how commands are generated. Can be
read_from_file, sample_from_file, or parametric_sample. The
read_from_file option takes the commands directly from the
command file. The sample_from_file option generates a
sequence of commands by sampling from the command file with
replacement. The parametric_sample option does not use the
command file. Rather, it samples commands uniformly from
the reach volume determined by user-selected inverse
kinematics parameters.
(y/z)gimbal_to_joint : instance
Tkinter StringVar that captures the user-measured
center-to-center distance (mm) from an y/z gimbal to its
respective spherical joint (link to hardware page).
(x/y/z)gimbal_(x/y/z)offset : instance
Tkinter StringVar that captures the user-measured offset (mm)
of the (x/y/z) gimbal from the origin in the (x/y/z)
direction.
(x/y/z)_origin : instance
Tkinter StringVar that captures the user-measured (x/y/z)
position (bits) of the origin.
reach_dist_(min/max) : instance
Tkinter StringVar that captures the user-specified (min/max)
reach distance of the `reach volume`.
reach_angle_max : instance
Tkinter StringVar that captures the user-specified max reach
angle (rad) of the `reach volume`.
"""
def __init__(self, parent):
#create window
tk.Toplevel.__init__(self, parent)
self.transient(parent)
self.grab_set()
self.title('Robot Settings')
self.configure(bg='white')
self.protocol('WM_DELETE_WINDOW', self.on_quit)
#initialize tk variables from config
self.config = config.load_config('./temp/tmp_config.json')
self.pos_smoothing = tk.StringVar()
self.pos_smoothing.set(str(self.config['RobotSettings']['pos_smoothing']))
self.tol = tk.StringVar()
self.tol.set(str(self.config['RobotSettings']['tol']))
self.period = tk.StringVar()
self.period.set(str(self.config['RobotSettings']['period']))
self.off_dur = tk.StringVar()
self.off_dur.set(str(self.config['RobotSettings']['off_dur']))
self.num_tol = tk.StringVar()
self.num_tol.set(str(self.config['RobotSettings']['num_tol']))
self.x_push_wt = tk.StringVar()
self.x_push_wt.set(str(self.config['RobotSettings']['x_push_wt']))
self.x_pull_wt = tk.StringVar()
self.x_pull_wt.set(str(self.config['RobotSettings']['x_pull_wt']))
self.y_push_wt = tk.StringVar()
self.y_push_wt.set(str(self.config['RobotSettings']['y_push_wt']))
self.y_pull_wt = tk.StringVar()
self.y_pull_wt.set(str(self.config['RobotSettings']['y_pull_wt']))
self.z_push_wt = tk.StringVar()
self.z_push_wt.set(str(self.config['RobotSettings']['z_push_wt']))
self.z_pull_wt = tk.StringVar()
self.z_pull_wt.set(str(self.config['RobotSettings']['z_pull_wt']))
self.rew_zone_x = tk.StringVar()
self.rew_zone_x.set(str(self.config['RobotSettings']['rew_zone_x']))
self.rew_zone_y_min = tk.StringVar()
self.rew_zone_y_min.set(str(self.config['RobotSettings']['rew_zone_y_min']))
self.rew_zone_y_max = tk.StringVar()
self.rew_zone_y_max.set(str(self.config['RobotSettings']['rew_zone_y_max']))
self.rew_zone_z_min = tk.StringVar()
self.rew_zone_z_min.set(str(self.config['RobotSettings']['rew_zone_z_min']))
self.rew_zone_z_max = tk.StringVar()
self.rew_zone_z_max.set(str(self.config['RobotSettings']['rew_zone_z_max']))
self.calibration_file = tk.StringVar()
self.calibration_file.set(str(self.config['RobotSettings']['calibration_file']))
self.command_file = tk.StringVar()
self.command_file.set(str(self.config['RobotSettings']['command_file']))
self.command_type = tk.StringVar()
self.command_type.set(self.config['RobotSettings']['command_type'])
self.ygimbal_to_joint = tk.StringVar()
self.ygimbal_to_joint.set(str(self.config['RobotSettings']['ygimbal_to_joint']))
self.zgimbal_to_joint = tk.StringVar()
self.zgimbal_to_joint.set(str(self.config['RobotSettings']['zgimbal_to_joint']))
self.xgimbal_xoffset = tk.StringVar()
self.xgimbal_xoffset.set(str(self.config['RobotSettings']['xgimbal_xoffset']))
self.ygimbal_yoffset = tk.StringVar()
self.ygimbal_yoffset.set(str(self.config['RobotSettings']['ygimbal_yoffset']))
self.zgimbal_zoffset = tk.StringVar()
self.zgimbal_zoffset.set(str(self.config['RobotSettings']['zgimbal_zoffset']))
self.x_origin = tk.StringVar()
self.x_origin.set(str(self.config['RobotSettings']['x_origin']))
self.y_origin = tk.StringVar()
self.y_origin.set(str(self.config['RobotSettings']['y_origin']))
self.z_origin = tk.StringVar()
self.z_origin.set(str(self.config['RobotSettings']['z_origin']))
self.reach_dist_min = tk.StringVar()
self.reach_dist_min.set(str(self.config['RobotSettings']['reach_dist_min']))
self.reach_dist_max = tk.StringVar()
self.reach_dist_max.set(str(self.config['RobotSettings']['reach_dist_max']))
self.reach_angle_max = tk.StringVar()
self.reach_angle_max.set(str(self.config['RobotSettings']['reach_angle_max']))
#configure window
self._configure_window()
[docs] def on_quit(self):
"""Called prior to destruction of the robot settings window.
Prior to destruction, the configuration file must be updated
to reflect the change in settings.
"""
self.config['RobotSettings']['pos_smoothing'] = float(self.pos_smoothing.get())
self.config['RobotSettings']['tol'] = float(self.tol.get())
self.config['RobotSettings']['period'] = float(self.period.get())
self.config['RobotSettings']['off_dur'] = int(self.off_dur.get())
self.config['RobotSettings']['num_tol'] = int(self.num_tol.get())
self.config['RobotSettings']['x_push_wt'] = float(self.x_push_wt.get())
self.config['RobotSettings']['x_pull_wt'] = float(self.x_pull_wt.get())
self.config['RobotSettings']['y_push_wt'] = float(self.y_push_wt.get())
self.config['RobotSettings']['y_pull_wt'] = float(self.y_pull_wt.get())
self.config['RobotSettings']['z_push_wt'] = float(self.z_push_wt.get())
self.config['RobotSettings']['z_pull_wt'] = float(self.z_pull_wt.get())
self.config['RobotSettings']['rew_zone_x'] = int(self.rew_zone_x.get())
self.config['RobotSettings']['rew_zone_y_min'] = int(self.rew_zone_y_min.get())
self.config['RobotSettings']['rew_zone_y_max'] = int(self.rew_zone_y_max.get())
self.config['RobotSettings']['rew_zone_z_min'] = int(self.rew_zone_z_min.get())
self.config['RobotSettings']['rew_zone_z_max'] = int(self.rew_zone_z_max.get())
self.config['RobotSettings']['calibration_file'] = self.calibration_file.get()
self.config['RobotSettings']['command_file'] = self.command_file.get()
self.config['RobotSettings']['command_type'] = self.command_type.get()
self.config['RobotSettings']['ygimbal_to_joint'] = int(self.ygimbal_to_joint.get())
self.config['RobotSettings']['zgimbal_to_joint'] = int(self.zgimbal_to_joint.get())
self.config['RobotSettings']['xgimbal_xoffset'] = int(self.xgimbal_xoffset.get())
self.config['RobotSettings']['ygimbal_yoffset'] = int(self.ygimbal_yoffset.get())
self.config['RobotSettings']['zgimbal_zoffset'] = int(self.zgimbal_zoffset.get())
self.config['RobotSettings']['x_origin'] = int(self.x_origin.get())
self.config['RobotSettings']['y_origin'] = int(self.y_origin.get())
self.config['RobotSettings']['z_origin'] = int(self.z_origin.get())
self.config['RobotSettings']['reach_dist_min'] = int(self.reach_dist_min.get())
self.config['RobotSettings']['reach_dist_max'] = int(self.reach_dist_max.get())
self.config['RobotSettings']['reach_angle_max'] = float(self.reach_angle_max.get())
config.save_tmp(self.config)
self.destroy()
def _configure_window(self):
tk.Label(
self,
text = 'Position Smoothing:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row=1, column=0)
tk.Entry(self, textvariable = self.pos_smoothing, width = 17).grid(row = 1, column = 1)
tk.Label(
self,
text = 'Valve Period (usec):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row=2, column=0)
tk.Entry(self, textvariable = self.period, width = 17).grid(row = 2, column = 1)
tk.Label(
self,
text = 'Off Duration (msec):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 3, column = 0)
tk.Entry(self, textvariable = self.off_dur, width = 17).grid(row = 3, column = 1)
tk.Label(
self,
text = 'Converge Tolerance (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 4, column = 0)
tk.Entry(self, textvariable = self.tol, width = 17).grid(row = 4, column = 1)
tk.Label(
self,
text = '# w/in Tolerance:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 5, column = 0)
tk.Entry(self, textvariable = self.num_tol, width = 17).grid(row = 5, column = 1)
tk.Label(
self,
text = 'X Push Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 6, column = 0)
tk.Entry(self, textvariable = self.x_push_wt, width = 17).grid(row = 6, column = 1)
tk.Label(
self,
text = 'X Pull Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 7, column = 0)
tk.Entry(self, textvariable = self.x_pull_wt, width = 17).grid(row = 7, column = 1)
tk.Label(
self,
text = 'Y Push Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 8, column = 0)
tk.Entry(self, textvariable = self.y_push_wt, width = 17).grid(row = 8, column = 1)
tk.Label(self,
text = 'Y Pull Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 9, column = 0)
tk.Entry(self, textvariable = self.y_pull_wt, width = 17).grid(row = 9, column = 1)
tk.Label(
self,
text = 'Z Push Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 10, column = 0)
tk.Entry(self, textvariable = self.z_push_wt, width = 17).grid(row = 10, column = 1)
tk.Label(
self,
text = 'Z Pull Weight:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,anchor='e'
).grid(row = 11, column = 0)
tk.Entry(self, textvariable = self.z_pull_wt, width = 17).grid(row = 11, column = 1)
tk.Label(
self,
text = 'Reward Zone X Min (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 12, column = 0)
tk.Entry(self, textvariable = self.rew_zone_x, width = 17).grid(row = 12, column = 1)
tk.Label(
self,
text = 'Reward Zone Y Min (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 13, column = 0)
tk.Entry(self, textvariable = self.rew_zone_y_min, width = 17).grid(row = 13, column = 1)
tk.Label(
self,
text = 'Reward Zone Y Max (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 14, column = 0)
tk.Entry(self, textvariable = self.rew_zone_y_max, width = 17).grid(row = 14, column = 1)
tk.Label(
self,
text = 'Reward Zone Z Min (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 15, column = 0)
tk.Entry(self, textvariable = self.rew_zone_z_min, width = 17).grid(row = 15, column = 1)
tk.Label(
self,
text = 'Reward Zone Z Max (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 16, column = 0)
tk.Entry(self, textvariable = self.rew_zone_z_max, width = 17).grid(row = 16, column = 1)
tk.Button(
self,
text = 'Run Calibration',
font = 'Arial 10 bold',
width = 14,
command = self.run_calibration_callback
).grid(row = 1, column = 5)
tk.Label(
self,
text = 'Calibration File:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 2, column = 4)
tk.Label(self, textvariable = self.calibration_file, bg = 'white').grid(row = 2, column = 5)
tk.Button(
self,
text = 'Browse',
font = 'Arial 10 bold',
width = 14,
command = self.calibration_browse_callback
).grid(row = 2, column = 6)
tk.Label(
self,
text = 'Command File:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 3, column = 4)
tk.Label(self, textvariable = self.command_file, bg = 'white').grid(row = 3, column = 5)
tk.Button(
self,
text = 'Browse',
font = 'Arial 10 bold',
width = 14,
command = self.command_browse_callback
).grid(row = 3, column = 6)
tk.Label(
self,
text = 'Command Type:',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 4, column = 4)
self.command_type_menu = tk.OptionMenu(
self,
self.command_type,
'read_from_file',
'sample_from_file',
'parametric_sample'
)
self.command_type_menu.configure(width = 26)
self.command_type_menu.grid(row = 4, column = 5)
tk.Label(
self,
text = 'Min Reach Distance (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 5, column = 4)
tk.Entry(self, textvariable = self.reach_dist_min, width = 17).grid(row = 5, column = 5)
tk.Label(
self,
text = 'Max Reach Distance (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 6, column = 4)
tk.Entry(self, textvariable = self.reach_dist_max, width = 17).grid(row = 6, column = 5)
tk.Label(
self,
text = 'Max Reach Angle (rad):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 7, column = 4)
tk.Entry(self, textvariable = self.reach_angle_max, width = 17).grid(row = 7, column = 5)
tk.Label(
self,
text = 'Y Gimbal to Joint (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 8, column = 4)
tk.Entry(self, textvariable = self.ygimbal_to_joint, width = 17).grid(row = 8, column = 5)
tk.Label(
self,
text = 'Z Gimbal to Joint (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 9, column = 4)
tk.Entry(self, textvariable = self.zgimbal_to_joint, width = 17).grid(row = 9, column = 5)
tk.Label(
self,
text = 'X Gimbal X Offset (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 10, column = 4)
tk.Entry(self, textvariable = self.xgimbal_xoffset, width = 17).grid(row = 10, column = 5)
tk.Label(
self,
text = 'Y Gimbal Y Offset (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 11, column = 4)
tk.Entry(self, textvariable = self.ygimbal_yoffset, width = 17).grid(row = 11, column = 5)
tk.Label(
self,
text = 'Z Gimbal Z Offset (mm):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 12, column = 4)
tk.Entry(self, textvariable = self.zgimbal_zoffset, width = 17).grid(row = 12, column = 5)
tk.Label(
self,
text = 'X Origin (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 13, column = 4)
tk.Entry(self, textvariable = self.x_origin, width = 17).grid(row = 13, column = 5)
tk.Label(
self,
text = 'Y Origin (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 14, column = 4)
tk.Entry(self, textvariable = self.y_origin, width = 17).grid(row = 14, column = 5)
tk.Label(
self,
text = 'Z Origin (bits):',
font = 'Arial 10 bold',
bg = 'white',
width = 26,
anchor = 'e'
).grid(row = 15, column = 4)
tk.Entry(self, textvariable = self.z_origin, width = 17).grid(row = 15, column = 5)
# Callbacks -----------------------------------------------------------------------------------------
[docs] def run_calibration_callback(self):
"""Not yet implented. Should load the robot calibration script to
the robot microcontroller along with any relevant arguments,
run all calibration routines, and produce a calibration file."""
print('not implemented')
[docs] def calibration_browse_callback(self):
"""Allows the user to select a calibration file to be loaded to the
robot microcontroller."""
self.calibration_file.set(tkinter.filedialog.askopenfilename())
self.config['RobotSettings']['calibration_file'] = self.calibration_file.get()
[docs] def command_browse_callback(self):
"""Allows the user to select a position command file to be loaded
to the robot microcontroller."""
self.command_file.set(tkinter.filedialog.askopenfilename())
self.config['RobotSettings']['command_file'] = self.command_file.get()