def addToGUI(self, frame, row, disable_all_buttons, mainGUI): self._run_button = Button( frame, text=self._button_text, command=lambda: ( disable_all_buttons(), self._status_text_val.set("Running..."), self._on_click(), ), state="disabled", ) self._run_button.grid(row=row, column=0, sticky="ew") status = Entry(frame, textvariable=self._status_text_val) status.configure(state="readonly") status.grid(row=row, column=1) if self._option_label != None and self._option_val != None: Label(frame, text=self._option_label, anchor="e").grid(row=row, column=2, sticky="ew") FloatEntry(frame, textvariable=self._option_val).grid(row=row, column=3, sticky="ew")
def configure_gui(STATE): def getFile(): dataFile = tkinter.filedialog.askopenfile( parent=STATE.mainGUI, mode="rb", title="Choose the data file (.JSON)", initialdir=STATE.project_path.get(), ) fileEntry.configure(state="normal") fileEntry.delete(0, END) fileEntry.insert(0, dataFile.name) fileEntry.configure(state="readonly") try: data = json.load(dataFile) try: # Transform the data into a numpy array to make it easier to use # -> transpose it so we can deal with it in columns for k in JSON_DATA_KEYS: data[k] = np.array(data[k]).transpose() STATE.stored_data = data analyzeButton.configure(state="normal") except Exception as e: messagebox.showerror( "Error!", "The structure of the data JSON was not recognized.\n" + "Details\n" + repr(e), ) return except Exception as e: messagebox.showerror( "Error!", "The JSON file could not be loaded.\n" + "Details:\n" + repr(e), parent=STATE.mainGUI, ) return def runAnalysis(): ( STATE.quasi_forward, STATE.quasi_backward, STATE.step_forward, STATE.step_backward, ) = prepare_data(STATE.stored_data, window=STATE.window_size.get(), STATE=STATE) if (STATE.quasi_forward is None or STATE.quasi_backward is None or STATE.step_forward is None or STATE.step_backward is None): return if STATE.direction.get() == "Forward": kg, kfr, kv, ka, rsquare = calcFit(STATE.quasi_forward, STATE.step_forward) elif STATE.direction.get() == "Backward": kg, kfr, kv, ka, rsquare = calcFit(STATE.quasi_backward, STATE.step_backward) else: kg, kfr, kv, ka, rsquare = calcFit( np.concatenate((STATE.quasi_forward, STATE.quasi_backward), axis=1), np.concatenate((STATE.step_forward, STATE.step_backward), axis=1), ) STATE.kg.set(float("%.3g" % kg)) STATE.kfr.set(float("%.3g" % kfr)) STATE.kv.set(float("%.3g" % kv)) STATE.ka.set(float("%.3g" % ka)) STATE.r_square.set(float("%.3g" % rsquare)) calcGains() timePlotsButton.configure(state="normal") voltPlotsButton.configure(state="normal") fancyPlotButton.configure(state="normal") calcGainsButton.configure(state="normal") def plotTimeDomain(): if STATE.direction.get() == "Forward": _plotTimeDomain("Forward", STATE.quasi_forward, STATE.step_forward) elif STATE.direction.get() == "Backward": _plotTimeDomain("Backward", STATE.quasi_backward, STATE.step_backward) else: _plotTimeDomain( "Combined", np.concatenate((STATE.quasi_forward, STATE.quasi_backward), axis=1), np.concatenate((STATE.step_forward, STATE.step_backward), axis=1), ) def plotVoltageDomain(): if STATE.direction.get() == "Forward": _plotVoltageDomain("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.direction.get() == "Backward": _plotVoltageDomain("Backward", STATE.quasi_backward, STATE.step_backward, STATE) else: _plotVoltageDomain( "Combined", np.concatenate((STATE.quasi_forward, STATE.quasi_backward), axis=1), np.concatenate((STATE.step_forward, STATE.step_backward), axis=1), STATE, ) def plot3D(): if STATE.direction.get() == "Forward": _plot3D("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.direction.get() == "Backward": _plot3D("Backward", STATE.quasi_backward, STATE.step_backward, STATE) else: _plot3D( "Combined", np.concatenate((STATE.quasi_forward, STATE.quasi_backward), axis=1), np.concatenate((STATE.step_forward, STATE.step_backward), axis=1), STATE, ) def calcGains(): period = (STATE.period.get() if not STATE.has_slave.get() else STATE.slave_period.get()) kp, kd = _calcGains( STATE.kv.get(), STATE.ka.get(), STATE.qp.get(), STATE.qv.get(), STATE.max_effort.get(), period, STATE.measurement_delay.get(), ) # Scale gains to output kp = kp / 12 * STATE.max_controller_output.get() kd = kd / 12 * STATE.max_controller_output.get() # Rescale kD if not time-normalized if not STATE.controller_time_normalized.get(): kd = kd / STATE.period.get() # Get correct conversion factor for rotations if STATE.units.get() == "Degrees": rotation = 360 elif STATE.units.get() == "Radians": rotation = 2 * math.pi elif STATE.units.get() == "Rotations": rotation = 1 else: rotation = STATE.pulley_diam.get() * math.pi # Convert to motor-controller native units if STATE.controller_type.get() == "Talon": kp = kp * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) kd = kd * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) STATE.kp.set(float("%.3g" % kp)) STATE.kd.set(float("%.3g" % kd)) def presetGains(*args): # Note that all the delays are zero because the elevator characterizer only runs in position mode and most motor controllers do not have non-CAN (i.e. filtering) delay in position mode presets = { "Default": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), STATE.measurement_delay.set(0), ), "WPILib (2020-)": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), # Note that the user will need to remember to set this if the onboard controller is getting delayed measurements STATE.measurement_delay.set(0), ), "WPILib (Pre-2020)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.05), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Onboard"), # Note that the user will need to remember to set this if the onboard controller is getting delayed measurements STATE.measurement_delay.set(0), ), "Talon FX": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), STATE.measurement_delay.set(0), ), "Talon SRX (2020-)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), STATE.measurement_delay.set(0), ), "Talon SRX (Pre-2020)": lambda: ( STATE.max_controller_output.set(1023), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Talon"), STATE.measurement_delay.set(0), ), "Spark MAX (brushless)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), STATE.measurement_delay.set(0), ), "Spark MAX (brushed)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), STATE.measurement_delay.set(0), ), } presets.get(STATE.gain_units_preset.get(), "Default")() def enablePulleyDiam(*args): if (STATE.units.get() == "Feet" or STATE.units.get() == "Inches" or STATE.units.get() == "Meters"): diamEntry.configure(state="normal") else: diamEntry.configure(state="disabled") def enableOffboard(*args): if STATE.controller_type.get() == "Onboard": gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="disabled") slavePeriodEntry.configure(state="disabled") elif STATE.controller_type.get() == "Talon": gearingEntry.configure(state="normal") eprEntry.configure(state="normal") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") else: gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") # TOP OF WINDOW (FILE SELECTION) topFrame = Frame(STATE.mainGUI) topFrame.grid(row=0, column=0, columnspan=4) Button(topFrame, text="Select Data File", command=getFile).grid(row=0, column=0) fileEntry = Entry(topFrame, width=80) fileEntry.grid(row=0, column=1, columnspan=3) fileEntry.configure(state="readonly") Label(topFrame, text="Units:", width=10).grid(row=0, column=4) unitChoices = { "Feet", "Inches", "Meters", "Degrees", "Radians", "Rotations" } unitsMenu = OptionMenu(topFrame, STATE.units, *sorted(unitChoices)) unitsMenu.configure(width=10) unitsMenu.grid(row=0, column=5, sticky="ew") STATE.units.trace_add("write", enablePulleyDiam) Label(topFrame, text="Pulley Diameter (units):", anchor="e").grid(row=1, column=3, columnspan=2, sticky="ew") diamEntry = FloatEntry(topFrame, textvariable=STATE.pulley_diam) diamEntry.configure(state="disabled") diamEntry.grid(row=1, column=5) Label(topFrame, text="Direction:", width=10).grid(row=0, column=6) directions = {"Combined", "Forward", "Backward"} dirMenu = OptionMenu(topFrame, STATE.direction, *sorted(directions)) dirMenu.configure(width=10) dirMenu.grid(row=0, column=7) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDFORWARD ANALYSIS FRAME ffFrame = Frame(STATE.mainGUI, bd=2, relief="groove") ffFrame.grid(row=1, column=0, columnspan=3, sticky="ns") Label(ffFrame, text="Feedforward Analysis").grid(row=0, column=0, columnspan=5) analyzeButton = Button(ffFrame, text="Analyze Data", command=runAnalysis, state="disabled") analyzeButton.grid(row=1, column=0, sticky="ew") timePlotsButton = Button( ffFrame, text="Time-Domain Diagnostics", command=plotTimeDomain, state="disabled", ) timePlotsButton.grid(row=2, column=0, sticky="ew") voltPlotsButton = Button( ffFrame, text="Voltage-Domain Diagnostics", command=plotVoltageDomain, state="disabled", ) voltPlotsButton.grid(row=3, column=0, sticky="ew") fancyPlotButton = Button(ffFrame, text="3D Diagnostics", command=plot3D, state="disabled") fancyPlotButton.grid(row=4, column=0, sticky="ew") Label(ffFrame, text="Accel Window Size:", anchor="e").grid(row=1, column=1, sticky="ew") windowEntry = IntEntry(ffFrame, textvariable=STATE.window_size, width=5) windowEntry.grid(row=1, column=2) Label(ffFrame, text="Motion Threshold (units/s):", anchor="e").grid(row=2, column=1, sticky="ew") thresholdEntry = FloatEntry(ffFrame, textvariable=STATE.motion_threshold, width=5) thresholdEntry.grid(row=2, column=2) Label(ffFrame, text="kG:", anchor="e").grid(row=1, column=3, sticky="ew") kGEntry = FloatEntry(ffFrame, textvariable=STATE.kg, width=10) kGEntry.grid(row=1, column=4) kGEntry.configure(state="readonly") Label(ffFrame, text="kFr:", anchor="e").grid(row=2, column=3, sticky="ew") kFrEntry = FloatEntry(ffFrame, textvariable=STATE.kfr, width=10) kFrEntry.grid(row=2, column=4) kFrEntry.configure(state="readonly") Label(ffFrame, text="kV:", anchor="e").grid(row=3, column=3, sticky="ew") kVEntry = FloatEntry(ffFrame, textvariable=STATE.kv, width=10) kVEntry.grid(row=3, column=4) kVEntry.configure(state="readonly") Label(ffFrame, text="kA:", anchor="e").grid(row=4, column=3, sticky="ew") kAEntry = FloatEntry(ffFrame, textvariable=STATE.ka, width=10) kAEntry.grid(row=4, column=4) kAEntry.configure(state="readonly") Label(ffFrame, text="r-squared:", anchor="e").grid(row=5, column=3, sticky="ew") rSquareEntry = FloatEntry(ffFrame, textvariable=STATE.r_square, width=10) rSquareEntry.grid(row=5, column=4) rSquareEntry.configure(state="readonly") for child in ffFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDBACK ANALYSIS FRAME fbFrame = Frame(STATE.mainGUI, bd=2, relief="groove") fbFrame.grid(row=1, column=3, columnspan=5) Label(fbFrame, text="Feedback Analysis").grid(row=0, column=0, columnspan=5) Label(fbFrame, text="Gain Settings Preset:", anchor="e").grid(row=1, column=0, sticky="ew") presetChoices = { "Default", "WPILib (2020-)", "WPILib (Pre-2020)", "Talon FX", "Talon SRX (2020-)", "Talon SRX (Pre-2020)", "Spark MAX (brushless)", "Spark MAX (brushed)", } presetMenu = OptionMenu(fbFrame, STATE.gain_units_preset, *sorted(presetChoices)) presetMenu.grid(row=1, column=1) presetMenu.config(width=12) STATE.gain_units_preset.trace_add("write", presetGains) Label(fbFrame, text="Controller Period (s):", anchor="e").grid(row=2, column=0, sticky="ew") periodEntry = FloatEntry(fbFrame, textvariable=STATE.period, width=10) periodEntry.grid(row=2, column=1) Label(fbFrame, text="Max Controller Output:", anchor="e").grid(row=3, column=0, sticky="ew") controllerMaxEntry = FloatEntry(fbFrame, textvariable=STATE.max_controller_output, width=10) controllerMaxEntry.grid(row=3, column=1) Label(fbFrame, text="Time-Normalized Controller:", anchor="e").grid(row=4, column=0, sticky="ew") normalizedButton = Checkbutton(fbFrame, variable=STATE.controller_time_normalized) normalizedButton.grid(row=4, column=1) Label(fbFrame, text="Controller Type:", anchor="e").grid(row=5, column=0, sticky="ew") controllerTypes = {"Onboard", "Talon", "Spark"} controllerTypeMenu = OptionMenu(fbFrame, STATE.controller_type, *sorted(controllerTypes)) controllerTypeMenu.grid(row=5, column=1) STATE.controller_type.trace_add("write", enableOffboard) Label(fbFrame, text="Measurement delay (ms):", anchor="e").grid(row=6, column=0, sticky="ew") velocityDelay = FloatEntry(fbFrame, textvariable=STATE.measurement_delay, width=10) velocityDelay.grid(row=6, column=1) Label(fbFrame, text="Post-Encoder Gearing:", anchor="e").grid(row=7, column=0, sticky="ew") gearingEntry = FloatEntry(fbFrame, textvariable=STATE.gearing, width=10) gearingEntry.configure(state="disabled") gearingEntry.grid(row=7, column=1) Label(fbFrame, text="Encoder EPR:", anchor="e").grid(row=8, column=0, sticky="ew") eprEntry = IntEntry(fbFrame, textvariable=STATE.encoder_epr, width=10) eprEntry.configure(state="disabled") eprEntry.grid(row=8, column=1) Label(fbFrame, text="Has Slave:", anchor="e").grid(row=9, column=0, sticky="ew") hasSlave = Checkbutton(fbFrame, variable=STATE.has_slave) hasSlave.grid(row=9, column=1) hasSlave.configure(state="disabled") STATE.has_slave.trace_add("write", enableOffboard) Label(fbFrame, text="Slave Update Period (s):", anchor="e").grid(row=10, column=0, sticky="ew") slavePeriodEntry = FloatEntry(fbFrame, textvariable=STATE.slave_period, width=10) slavePeriodEntry.grid(row=10, column=1) slavePeriodEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Position Error (units):", anchor="e").grid(row=1, column=2, columnspan=2, sticky="ew") qPEntry = FloatEntry(fbFrame, textvariable=STATE.qp, width=10) qPEntry.grid(row=1, column=4) Label(fbFrame, text="Max Acceptable Velocity Error (units/s):", anchor="e").grid(row=2, column=2, columnspan=2, sticky="ew") qVEntry = FloatEntry(fbFrame, textvariable=STATE.qv, width=10) qVEntry.grid(row=2, column=4) Label(fbFrame, text="Max Acceptable Control Effort (V):", anchor="e").grid(row=3, column=2, columnspan=2, sticky="ew") effortEntry = FloatEntry(fbFrame, textvariable=STATE.max_effort, width=10) effortEntry.grid(row=3, column=4) Label(fbFrame, text="kV:", anchor="e").grid(row=5, column=2, sticky="ew") kVFBEntry = FloatEntry(fbFrame, textvariable=STATE.kv, width=10) kVFBEntry.grid(row=5, column=3) Label(fbFrame, text="kA:", anchor="e").grid(row=6, column=2, sticky="ew") kAFBEntry = FloatEntry(fbFrame, textvariable=STATE.ka, width=10) kAFBEntry.grid(row=6, column=3) calcGainsButton = Button( fbFrame, text="Calculate Optimal Controller Gains", command=calcGains, state="disabled", ) calcGainsButton.grid(row=7, column=2, columnspan=3) Label(fbFrame, text="kP:", anchor="e").grid(row=8, column=2, sticky="ew") kPEntry = FloatEntry(fbFrame, textvariable=STATE.kp, width=10, state="readonly").grid(row=8, column=3) Label(fbFrame, text="kD:", anchor="e").grid(row=9, column=2, sticky="ew") kDEntry = FloatEntry(fbFrame, textvariable=STATE.kd, width=10, state="readonly").grid(row=9, column=3) for child in fbFrame.winfo_children(): child.grid_configure(padx=1, pady=1) enableOffboard() enablePulleyDiam()
def configure_gui(STATE): def getFile(): dataFile = tkinter.filedialog.askopenfile( parent=STATE.mainGUI, mode="rb", title="Choose the data file (.JSON)", initialdir=STATE.project_path.get(), ) fileEntry.configure(state="normal") fileEntry.delete(0, END) fileEntry.insert(0, dataFile.name) fileEntry.configure(state="readonly") try: data = json.load(dataFile) try: # Transform the data into a numpy array to make it easier to use # -> transpose it so we can deal with it in columns for k in JSON_DATA_KEYS: data[k] = np.array(data[k]).transpose() STATE.stored_data = data analyzeButton.configure(state="normal") except Exception as e: messagebox.showerror( "Error!", "The structure of the data JSON was not recognized.\n" + "Details\n" + repr(e), ) return except Exception as e: messagebox.showerror( "Error!", "The JSON file could not be loaded.\n" + "Details:\n" + repr(e), parent=STATE.mainGUI, ) return def runAnalysis(): ( STATE.quasi_forward_l, STATE.quasi_backward_l, STATE.step_forward_l, STATE.step_backward_l, STATE.quasi_forward_r, STATE.quasi_backward_r, STATE.step_forward_r, STATE.step_backward_r, ) = prepare_data(STATE.stored_data, window=STATE.window_size.get(), STATE=STATE) if (STATE.quasi_forward_l is None or STATE.quasi_backward_l is None or STATE.step_forward_l is None or STATE.step_backward_l is None or STATE.quasi_forward_r is None or STATE.quasi_backward_r is None or STATE.step_forward_r is None or STATE.step_backward_r is None): return if STATE.subset.get() == "Forward Left": ks, kv, ka, rsquare = calcFit(STATE.quasi_forward_l, STATE.step_forward_l) elif STATE.subset.get() == "Forward Right": ks, kv, ka, rsquare = calcFit(STATE.quasi_forward_r, STATE.step_forward_r) elif STATE.subset.get() == "Backward Left": ks, kv, ka, rsquare = calcFit(STATE.quasi_backward_l, STATE.step_backward_l) elif STATE.subset.get() == "Backward Right": ks, kv, ka, rsquare = calcFit(STATE.quasi_backward_r, STATE.step_backward_r) elif STATE.subset.get() == "Forward Combined": ks, kv, ka, rsquare = calcFit( np.concatenate((STATE.quasi_forward_l, STATE.quasi_forward_r), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_forward_r), axis=1), ) elif STATE.subset.get() == "Backward Combined": ks, kv, ka, rsquare = calcFit( np.concatenate( (STATE.quasi_backward_l, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_backward_l, STATE.step_backward_r), axis=1), ) elif STATE.subset.get() == "Left Combined": ks, kv, ka, rsquare = calcFit( np.concatenate((STATE.quasi_forward_l, STATE.quasi_backward_l), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_backward_l), axis=1), ) elif STATE.subset.get() == "Right Combined": ks, kv, ka, rsquare = calcFit( np.concatenate((STATE.quasi_forward_r, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_forward_r, STATE.step_backward_r), axis=1), ) else: ks, kv, ka, rsquare = calcFit( np.concatenate( ( STATE.quasi_forward_l, STATE.quasi_forward_r, STATE.quasi_backward_l, STATE.quasi_backward_r, ), axis=1, ), np.concatenate( ( STATE.step_forward_l, STATE.step_forward_r, STATE.step_backward_l, STATE.step_backward_r, ), axis=1, ), ) STATE.ks.set(float("%.3g" % ks)) STATE.kv.set(float("%.3g" % kv)) STATE.ka.set(float("%.3g" % ka)) STATE.r_square.set(float("%.3g" % rsquare)) if "track-width" in STATE.stored_data: STATE.track_width.set( calcTrackWidth(STATE.stored_data["track-width"])) else: STATE.track_width.set("N/A") calcGains() timePlotsButton.configure(state="normal") voltPlotsButton.configure(state="normal") fancyPlotButton.configure(state="normal") calcGainsButton.configure(state="normal") def plotTimeDomain(): if STATE.subset.get() == "Forward Left": _plotTimeDomain("Forward Left", STATE.quasi_forward_l, STATE.step_forward_l) elif STATE.subset.get() == "Forward Right": _plotTimeDomain("Forward Right", STATE.quasi_forward_r, STATE.step_forward_r) elif STATE.subset.get() == "Backward Left": _plotTimeDomain("Backward Left", STATE.quasi_backward_l, STATE.step_backward_l) elif STATE.subset.get() == "Backward Right": _plotTimeDomain("Backward Right", STATE.quasi_backward_r, STATE.step_backward_r) elif STATE.subset.get() == "Forward Combined": _plotTimeDomain( "Forward Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_forward_r), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_forward_r), axis=1), ) elif STATE.subset.get() == "Backward Combined": _plotTimeDomain( "Backward Combined", np.concatenate( (STATE.quasi_backward_l, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_backward_l, STATE.step_backward_r), axis=1), ) elif STATE.subset.get() == "Left Combined": _plotTimeDomain( "Left Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_backward_l), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_backward_l), axis=1), ) elif STATE.subset.get() == "Right Combined": _plotTimeDomain( "Right Combined", np.concatenate((STATE.quasi_forward_r, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_forward_r, STATE.step_backward_r), axis=1), ) else: _plotTimeDomain( "All Combined", np.concatenate( ( STATE.quasi_forward_l, STATE.quasi_forward_r, STATE.quasi_backward_l, STATE.quasi_backward_r, ), axis=1, ), np.concatenate( ( STATE.step_forward_l, STATE.step_forward_r, STATE.step_backward_l, STATE.step_backward_r, ), axis=1, ), ) def plotVoltageDomain(): if STATE.subset.get() == "Forward Left": _plotVoltageDomain("Forward Left", STATE.quasi_forward_l, STATE.step_forward_l, STATE), elif STATE.subset.get() == "Forward Right": _plotVoltageDomain("Forward Right", STATE.quasi_forward_r, STATE.step_forward_r, STATE) elif STATE.subset.get() == "Backward Left": _plotVoltageDomain("Backward Left", STATE.quasi_backward_l, STATE.step_backward_l, STATE) elif STATE.subset.get() == "Backward Right": _plotVoltageDomain("Backward Right", STATE.quasi_backward_r, STATE.step_backward_r, STATE) elif STATE.subset.get() == "Forward Combined": _plotVoltageDomain( "Forward Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_forward_r), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_forward_r), axis=1), STATE, ) elif STATE.subset.get() == "Backward Combined": _plotVoltageDomain( "Backward Combined", np.concatenate( (STATE.quasi_backward_l, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_backward_l, STATE.step_backward_r), axis=1), STATE, ) elif STATE.subset.get() == "Left Combined": _plotVoltageDomain( "Left Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_backward_l), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_backward_l), axis=1), STATE, ) elif STATE.subset.get() == "Right Combined": _plotVoltageDomain( "Right Combined", np.concatenate((STATE.quasi_forward_r, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_forward_r, STATE.step_backward_r), axis=1), STATE, ) else: _plotVoltageDomain( "All Combined", np.concatenate( ( STATE.quasi_forward_l, STATE.quasi_forward_r, STATE.quasi_backward_l, STATE.quasi_backward_r, ), axis=1, ), np.concatenate( ( STATE.step_forward_l, STATE.step_forward_r, STATE.step_backward_l, STATE.step_backward_r, ), axis=1, ), STATE, ) def plot3D(): if STATE.subset.get() == "Forward Left": _plot3D("Forward Left", STATE.quasi_forward_l, STATE.step_forward_l, STATE) elif STATE.subset.get() == "Forward Right": _plot3D("Forward Right", STATE.quasi_forward_r, STATE.step_forward_r, STATE) elif STATE.subset.get() == "Backward Left": _plot3D("Backward Left", STATE.quasi_backward_l, STATE.step_backward_l, STATE) elif STATE.subset.get() == "Backward Right": _plot3D("Backward Right", STATE.quasi_backward_r, STATE.step_backward_r, STATE) elif STATE.subset.get() == "Forward Combined": _plot3D( "Forward Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_forward_r), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_forward_r), axis=1), STATE, ) elif STATE.subset.get() == "Backward Combined": _plot3D( "Backward Combined", np.concatenate( (STATE.quasi_backward_l, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_backward_l, STATE.step_backward_r), axis=1), STATE, ) elif STATE.subset.get() == "Left Combined": _plot3D( "Left Combined", np.concatenate((STATE.quasi_forward_l, STATE.quasi_backward_l), axis=1), np.concatenate((STATE.step_forward_l, STATE.step_backward_l), axis=1), STATE, ) elif STATE.subset.get() == "Right Combined": _plot3D( "Right Combined", np.concatenate((STATE.quasi_forward_r, STATE.quasi_backward_r), axis=1), np.concatenate((STATE.step_forward_r, STATE.step_backward_r), axis=1), STATE, ) else: _plot3D( "All Combined", np.concatenate( ( STATE.quasi_forward_l, STATE.quasi_forward_r, STATE.quasi_backward_l, STATE.quasi_backward_r, ), axis=1, ), np.concatenate( ( STATE.step_forward_l, STATE.step_forward_r, STATE.step_backward_l, STATE.step_backward_r, ), axis=1, ), STATE, ) def calcGains(): period = (STATE.period.get() if not STATE.has_slave.get() else STATE.slave_period.get()) if STATE.loop_type.get() == "Position": kp, kd = _calcGainsPos( STATE.kv.get(), STATE.ka.get(), STATE.qp.get(), STATE.qv.get(), STATE.max_effort.get(), period, ) else: kp, kd = _calcGainsVel( STATE.kv.get(), STATE.ka.get(), STATE.qv.get(), STATE.max_effort.get(), period, ) # Scale gains to output kp = kp / 12 * STATE.max_controller_output.get() kd = kd / 12 * STATE.max_controller_output.get() # Rescale kD if not time-normalized if not STATE.controller_time_normalized.get(): kd = kd / STATE.period.get() # Get correct conversion factor for rotations if STATE.units.get() == "Radians": rotation = 2 * math.pi elif STATE.units.get() == "Rotations": rotation = 1 else: rotation = STATE.wheel_diam.get() * math.pi # Convert to controller-native units if STATE.controller_type.get() == "Talon": kp = kp * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) kd = kd * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) if STATE.loop_type.get() == "Velocity": kp = kp * 10 STATE.kp.set(float("%.3g" % kp)) STATE.kd.set(float("%.3g" % kd)) def calcTrackWidth(table): # Note that this assumes the gyro angle is not modded (i.e. on [0, +infinity)), # and that a positive angle travels in the counter-clockwise direction d_left = table[-1][R_ENCODER_P_COL] - table[0][R_ENCODER_P_COL] d_right = table[-1][L_ENCODER_P_COL] - table[0][L_ENCODER_P_COL] d_angle = table[-1][GYRO_ANGLE_COL] - table[0][GYRO_ANGLE_COL] if d_angle == 0: messagebox.showerror( "Error!", "Change in gyro angle was 0... Is your gyro set up correctly?") return 0.0 # The below comes from solving ω=(vr−vl)/2r for 2r # Absolute values used to ensure the calculated value is always positive # and to add robustness to sensor inversion diameter = (abs(d_left) + abs(d_right)) / abs(d_angle) return diameter def presetGains(*args): presets = { "Default": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), ), "WPILib (2020-)": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), ), "WPILib (Pre-2020)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.05), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Onboard"), ), "Talon (2020-)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), ), "Talon (Pre-2020)": lambda: ( STATE.max_controller_output.set(1023), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Talon"), ), "Spark MAX": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), ), } presets.get(STATE.gain_units_preset.get(), "Default")() def enableOffboard(*args): if STATE.controller_type.get() == "Onboard": gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="disabled") slavePeriodEntry.configure(state="disabled") elif STATE.controller_type.get() == "Talon": gearingEntry.configure(state="normal") eprEntry.configure(state="normal") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") else: gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") def enableWheelDiam(*args): if (STATE.units.get() == "Feet" or STATE.units.get() == "Inches" or STATE.units.get() == "Meters"): diamEntry.configure(state="normal") else: diamEntry.configure(state="disabled") def enableErrorBounds(*args): if STATE.loop_type.get() == "Position": qPEntry.configure(state="normal") else: qPEntry.configure(state="disabled") # TOP OF WINDOW (FILE SELECTION) topFrame = Frame(STATE.mainGUI) topFrame.grid(row=0, column=0, columnspan=4) Button(topFrame, text="Select Data File", command=getFile).grid(row=0, column=0, padx=4) fileEntry = Entry(topFrame, width=80) fileEntry.grid(row=0, column=1, columnspan=3) fileEntry.configure(state="readonly") Label(topFrame, text="Units:", width=10).grid(row=0, column=4) unitChoices = {"Feet", "Inches", "Meters", "Radians", "Rotations"} unitsMenu = OptionMenu(topFrame, STATE.units, *sorted(unitChoices)) unitsMenu.configure(width=10) unitsMenu.grid(row=0, column=5, sticky="ew") STATE.units.trace_add("write", enableWheelDiam) Label(topFrame, text="Wheel Diameter (units):", anchor="e").grid(row=1, column=3, columnspan=2, sticky="ew") diamEntry = FloatEntry(topFrame, textvariable=STATE.wheel_diam) diamEntry.grid(row=1, column=5) Label(topFrame, text="Subset:", width=15).grid(row=0, column=6) subsets = { "All Combined", "Forward Left", "Forward Right", "Forward Combined", "Backward Left", "Backward Right", "Backward Combined", "Left Combined", "Right Combined", } dirMenu = OptionMenu(topFrame, STATE.subset, *sorted(subsets)) dirMenu.configure(width=20) dirMenu.grid(row=0, column=7) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDFORWARD ANALYSIS FRAME ffFrame = Frame(STATE.mainGUI, bd=2, relief="groove") ffFrame.grid(row=1, column=0, columnspan=3, sticky="ns") Label(ffFrame, text="Feedforward Analysis").grid(row=0, column=0, columnspan=5) analyzeButton = Button(ffFrame, text="Analyze Data", command=runAnalysis, state="disabled") analyzeButton.grid(row=1, column=0, sticky="ew") timePlotsButton = Button( ffFrame, text="Time-Domain Diagnostics", command=plotTimeDomain, state="disabled", ) timePlotsButton.grid(row=2, column=0, sticky="ew") voltPlotsButton = Button( ffFrame, text="Voltage-Domain Diagnostics", command=plotVoltageDomain, state="disabled", ) voltPlotsButton.grid(row=3, column=0, sticky="ew") fancyPlotButton = Button(ffFrame, text="3D Diagnostics", command=plot3D, state="disabled") fancyPlotButton.grid(row=4, column=0, sticky="ew") Label(ffFrame, text="Accel Window Size:", anchor="e").grid(row=1, column=1, sticky="ew") windowEntry = IntEntry(ffFrame, textvariable=STATE.window_size, width=5) windowEntry.grid(row=1, column=2) Label(ffFrame, text="Motion Threshold (units/s):", anchor="e").grid(row=2, column=1, sticky="ew") thresholdEntry = FloatEntry(ffFrame, textvariable=STATE.motion_threshold, width=5) thresholdEntry.grid(row=2, column=2) Label(ffFrame, text="kS:", anchor="e").grid(row=1, column=3, sticky="ew") kSEntry = FloatEntry(ffFrame, textvariable=STATE.ks, width=10) kSEntry.grid(row=1, column=4) kSEntry.configure(state="readonly") Label(ffFrame, text="kV:", anchor="e").grid(row=2, column=3, sticky="ew") kVEntry = FloatEntry(ffFrame, textvariable=STATE.kv, width=10) kVEntry.grid(row=2, column=4) kVEntry.configure(state="readonly") Label(ffFrame, text="kA:", anchor="e").grid(row=3, column=3, sticky="ew") kAEntry = FloatEntry(ffFrame, textvariable=STATE.ka, width=10) kAEntry.grid(row=3, column=4) kAEntry.configure(state="readonly") Label(ffFrame, text="r-squared:", anchor="e").grid(row=4, column=3, sticky="ew") rSquareEntry = FloatEntry(ffFrame, textvariable=STATE.r_square, width=10) rSquareEntry.grid(row=4, column=4) rSquareEntry.configure(state="readonly") Label(ffFrame, text="Track Width:", anchor="e").grid(row=5, column=3, sticky="ew") trackWidthEntry = FloatEntry(ffFrame, textvariable=STATE.track_width, width=10) trackWidthEntry.grid(row=5, column=4) trackWidthEntry.configure(state="readonly") for child in ffFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDBACK ANALYSIS FRAME fbFrame = Frame(STATE.mainGUI, bd=2, relief="groove") fbFrame.grid(row=1, column=3, columnspan=5) Label(fbFrame, text="Feedback Analysis").grid(row=0, column=0, columnspan=5) Label(fbFrame, text="Gain Settings Preset:", anchor="e").grid(row=1, column=0, sticky="ew") presetChoices = { "Default", "WPILib (2020-)", "WPILib (Pre-2020)", "Talon (2020-)", "Talon (Pre-2020)", "Spark MAX", } presetMenu = OptionMenu(fbFrame, STATE.gain_units_preset, *sorted(presetChoices)) presetMenu.grid(row=1, column=1) presetMenu.config(width=12) STATE.gain_units_preset.trace_add("write", presetGains) Label(fbFrame, text="Controller Period (s):", anchor="e").grid(row=2, column=0, sticky="ew") periodEntry = FloatEntry(fbFrame, textvariable=STATE.period, width=10) periodEntry.grid(row=2, column=1) Label(fbFrame, text="Max Controller Output:", anchor="e").grid(row=3, column=0, sticky="ew") controllerMaxEntry = FloatEntry(fbFrame, textvariable=STATE.max_controller_output, width=10) controllerMaxEntry.grid(row=3, column=1) Label(fbFrame, text="Time-Normalized Controller:", anchor="e").grid(row=4, column=0, sticky="ew") normalizedButton = Checkbutton(fbFrame, variable=STATE.controller_time_normalized) normalizedButton.grid(row=4, column=1) Label(fbFrame, text="Controller Type:", anchor="e").grid(row=5, column=0, sticky="ew") controllerTypes = {"Onboard", "Talon", "Spark"} controllerTypeMenu = OptionMenu(fbFrame, STATE.controller_type, *sorted(controllerTypes)) controllerTypeMenu.grid(row=5, column=1) STATE.controller_type.trace_add("write", enableOffboard) Label(fbFrame, text="Post-Encoder Gearing:", anchor="e").grid(row=6, column=0, sticky="ew") gearingEntry = FloatEntry(fbFrame, textvariable=STATE.gearing, width=10) gearingEntry.configure(state="disabled") gearingEntry.grid(row=6, column=1) Label(fbFrame, text="Encoder EPR:", anchor="e").grid(row=7, column=0, sticky="ew") eprEntry = IntEntry(fbFrame, textvariable=STATE.encoder_epr, width=10) eprEntry.configure(state="disabled") eprEntry.grid(row=7, column=1) Label(fbFrame, text="Has Slave:", anchor="e").grid(row=8, column=0, sticky="ew") hasSlave = Checkbutton(fbFrame, variable=STATE.has_slave) hasSlave.grid(row=8, column=1) hasSlave.configure(state="disabled") STATE.has_slave.trace_add("write", enableOffboard) Label(fbFrame, text="Slave Update Period (s):", anchor="e").grid(row=9, column=0, sticky="ew") slavePeriodEntry = FloatEntry(fbFrame, textvariable=STATE.slave_period, width=10) slavePeriodEntry.grid(row=9, column=1) slavePeriodEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Position Error (units):", anchor="e").grid(row=1, column=2, columnspan=2, sticky="ew") qPEntry = FloatEntry(fbFrame, textvariable=STATE.qp, width=10) qPEntry.grid(row=1, column=4) qPEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Velocity Error (units/s):", anchor="e").grid(row=2, column=2, columnspan=2, sticky="ew") qVEntry = FloatEntry(fbFrame, textvariable=STATE.qv, width=10) qVEntry.grid(row=2, column=4) Label(fbFrame, text="Max Acceptable Control Effort (V):", anchor="e").grid(row=3, column=2, columnspan=2, sticky="ew") effortEntry = FloatEntry(fbFrame, textvariable=STATE.max_effort, width=10) effortEntry.grid(row=3, column=4) Label(fbFrame, text="Loop Type:", anchor="e").grid(row=4, column=2, columnspan=2, sticky="ew") loopTypes = {"Position", "Velocity"} loopTypeMenu = OptionMenu(fbFrame, STATE.loop_type, *sorted(loopTypes)) loopTypeMenu.configure(width=8) loopTypeMenu.grid(row=4, column=4) STATE.loop_type.trace_add("write", enableErrorBounds) Label(fbFrame, text="kV:", anchor="e").grid(row=5, column=2, sticky="ew") kVFBEntry = FloatEntry(fbFrame, textvariable=STATE.kv, width=10) kVFBEntry.grid(row=5, column=3) Label(fbFrame, text="kA:", anchor="e").grid(row=6, column=2, sticky="ew") kAFBEntry = FloatEntry(fbFrame, textvariable=STATE.ka, width=10) kAFBEntry.grid(row=6, column=3) calcGainsButton = Button( fbFrame, text="Calculate Optimal Controller Gains", command=calcGains, state="disabled", ) calcGainsButton.grid(row=7, column=2, columnspan=3) Label(fbFrame, text="kP:", anchor="e").grid(row=8, column=2, sticky="ew") kPEntry = FloatEntry(fbFrame, textvariable=STATE.kp, width=10, state="readonly").grid(row=8, column=3) Label(fbFrame, text="kD:", anchor="e").grid(row=9, column=2, sticky="ew") kDEntry = FloatEntry(fbFrame, textvariable=STATE.kd, width=10, state="readonly").grid(row=9, column=3) for child in fbFrame.winfo_children(): child.grid_configure(padx=1, pady=1)
def configureGUI(self): mech = frc_characterization.logger_analyzer def getProjectLocation(): file_path = filedialog.askdirectory( title="Choose the project location", initialdir=self.project_path.get()) projLocationEntry.configure(state="normal") projLocationEntry.delete(0, END) projLocationEntry.insert(0, file_path) projLocationEntry.configure(state="readonly") def getConfigPath(): file_path = tkinter.filedialog.askopenfilename( title="Choose the config file", initialdir=self.project_path.get(), filetypes=(("Python", "*.py"), ), ) configEntry.configure(state="normal") configEntry.delete(0, END) configEntry.insert(0, file_path) configEntry.configure(state="readonly") def saveConfig(): with open(self.config_path.get(), "w+") as config: config.write(self.config.get()) def updateConfigPath(*args): configEntry.configure(state="normal") self.config_path.set( os.path.join(self.project_path.get(), "robotconfig.py")) configEntry.configure(state="readonly") def getDefaultConfig(*args): with resources.open_text( "frc_characterization.logger_analyzer.templates.configs", f"{self.control_type.get().lower()}config.py", ) as config: self.config.set(config.read()) def readConfig(): try: with open(self.config_path.get(), "r") as config: self.config.set(config.read()) except: messagebox.showerror("Error!", "Could not open/read config file.") return def genProject(): config = ast.literal_eval(self.config.get()) config["controlType"] = self.control_type.get() if self.control_type.get() == "SparkMax": config["controllerTypes"] = ["CANSparkMax"] config["rightControllerTypes"] = ["CANSparkMax"] logger.info(f"Config: {config}") dst = os.path.join(self.project_path.get(), "characterization-project") try: with resources.path(res, "project") as path: shutil.copytree(src=path, dst=dst) with open( os.path.join(dst, "src", "main", "java", "dc", "Robot.java"), "w+", ) as robot: robot.write(mech.gen_robot_code(config)) with open(os.path.join(dst, "build.gradle"), "w+") as build: build.write( mech.gen_build_gradle(self.team_number.get(), )) except FileExistsError: if messagebox.askyesno( "Warning!", "Project directory already exists! Do you want to overwrite it?", ): shutil.rmtree(dst) genProject() except Exception as e: messagebox.showerror( "Error!", "Unable to generate project - config may be bad.\n" + "Details:\n" + repr(e), ) shutil.rmtree(dst) def deployProject(queue): if self.team_number.get() == 0: cmd = "simulatejava" else: cmd = "deploy" def append_latest_jdk(process_args, jdk_base_path): possible_jdk_paths = glob.glob( os.path.join(jdk_base_path, "20[0-9][0-9]")) if len(possible_jdk_paths) <= 0: queue.put(( "Warning!", "You not appear to have any wpilib JDK installed. If your system JDK is the wrong version then the deploy will fail.", )) return year = max([ int(os.path.basename(path)) for path in possible_jdk_paths ]) process_args.append( "-Dorg.gradle.java.home=" + os.path.join(jdk_base_path, str(year), "jdk")) if int(year) != datetime.now().year: queue.put(( "Warning!", f"Your latest wpilib JDK's year ({year}) doesn't match the current year ({datetime.now().year}). Your deploy may fail.", )) if os.name == "nt": process_args = [ os.path.join( self.project_path.get(), "characterization-project", "gradlew.bat", ), cmd, "--console=plain", ] # C:/Users/Public/wpilib/YEAR/jdk is correct *as of* wpilib 2020 # Prior to 2020 the path was C:/Users/Public/frcYEAR/jdk jdk_base_path = os.path.join( os.path.abspath(os.path.join(os.path.expanduser("~"), "..")), "Public", "wpilib", ) append_latest_jdk(process_args, jdk_base_path) try: process = Popen( process_args, stdout=PIPE, stderr=STDOUT, cwd=os.path.join(self.project_path.get(), "characterization-project"), ) except Exception as e: queue.put(( "Error!", "Could not call gradlew deploy.\n" + "Details:\n" + repr(e), )) return else: process_args = [ os.path.join(self.project_path.get(), "characterization-project", "gradlew"), cmd, "--console=plain", ] # This path is correct *as of* wpilib 2020 # Prior to 2020 the path was ~/frcYEAR/jdk jdk_base_path = os.path.join(os.path.expanduser("~"), "wpilib") append_latest_jdk(process_args, jdk_base_path) try: process = Popen( process_args, stdout=PIPE, stderr=STDOUT, cwd=os.path.join(self.project_path.get(), "characterization-project"), ) except Exception as e: queue.put(( "Error!", "Could not call gradlew deploy.\n" + "Details:\n" + repr(e), )) return while process.poll() is None: time.sleep(0.1) queue.put(("Console", process.stdout.readline())) if process.poll() != 0: queue.put(( "Error!", "Deployment failed!\n" + "Check the console for more details.", )) # finish adding any outputs out = process.stdout.readline() while out: queue.put(("Console", out)) out = process.stdout.readline() def processError(message): if "Warning!" == message[0]: messagebox.showwarning(message[0], message[1], parent=self.deploy_window) else: messagebox.showerror(message[0], message[1], parent=self.deploy_window) def threadedDeploy(): logger.info("Starting Deploy") self.queue = queue.Queue() self.deploy_window = stdoutWindow() ThreadedTask(self.queue).start() self.mainGUI.after(10, processQueue) def processQueue(): try: msg = self.queue.get_nowait() if msg != "Task Finished": if msg[0] != "Console": processError(msg) else: updateStdout(msg[1]) self.mainGUI.after(10, processQueue) except queue.Empty: self.mainGUI.after(10, processQueue) class ThreadedTask(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): deployProject(self.queue) self.queue.put("Task Finished") logger.info("Finished Deploy") class stdoutWindow(tkinter.Toplevel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title("Deploy Progress") self.stdoutText = ScrolledText(self, width=60, height=15) self.stdoutText.grid(row=0, column=0) def updateStdout(out): if self.deploy_window.winfo_exists(): if out != "": self.deploy_window.stdoutText.insert(END, out) def runLogger(): mech.data_logger.main( self.team_number.get(), self.project_path.get(), units=Units(self.units.get()), units_per_rot=self.units_per_rot.get(), test=Tests(self.project_type.get()), ) def runAnalyzer(): mech.data_analyzer.main(self.project_path.get()) def enableUnitPerRot(*args): units = self.units.get() if isRotation(units): ureg = pint.UnitRegistry() units = Units(units) unitsRotationEntry.configure(state="readonly") self.units_per_rot.set( round((1 * ureg.revolution).to(units.unit).magnitude, 3)) else: self.units_per_rot.set(0) unitsRotationEntry.configure(state="normal") def isRotation(units): return Units(units) in (Units.ROTATIONS, Units.RADIANS, Units.DEGREES) getDefaultConfig() # TOP OF WINDOW topFrame = Frame(self.mainGUI) topFrame.grid(row=0, column=0, sticky="ew") Button(topFrame, text="Select Project Location", command=getProjectLocation).grid(row=0, column=0, sticky="ew") projLocationEntry = Entry(topFrame, textvariable=self.project_path, width=80, state="readonly") projLocationEntry.grid(row=0, column=1, columnspan=10) self.project_path.trace_add("write", updateConfigPath) Label(topFrame, text="Project Type:", anchor="e").grid(row=0, column=11, sticky="ew") projTypeMenu = OptionMenu(topFrame, self.project_type, *sorted(test.value for test in Tests)) projTypeMenu.configure(width=25) projTypeMenu.grid(row=0, column=12, sticky="ew") Button(topFrame, text="Select Config File", command=getConfigPath).grid(row=1, column=0, sticky="ew") configEntry = Entry(topFrame, textvariable=self.config_path, width=80, state="readonly") configEntry.grid(row=1, column=1, columnspan=10) Button(topFrame, text="Save Config", command=saveConfig).grid(row=1, column=11) readConfigButton = Button(topFrame, text="Read Config", command=readConfig) readConfigButton.grid(row=1, column=12, sticky="ew") Label(topFrame, text="Team Number:", anchor="e").grid(row=2, column=0, sticky="ew") teamNumberEntry = IntEntry(topFrame, textvariable=self.team_number) teamNumberEntry.grid(row=2, column=1, sticky="ew") Label(topFrame, text="Unit Type:", anchor="e").grid(row=2, column=2, sticky="ew") unitMenu = OptionMenu(topFrame, self.units, *sorted(unit.value for unit in Units)) unitMenu.configure(width=25) unitMenu.grid(row=2, column=3, sticky="ew") self.units.trace_add("write", enableUnitPerRot) Label(topFrame, text="Units per Rotation:", anchor="e").grid(row=2, column=4, sticky="ew") unitsRotationEntry = FloatEntry(topFrame, textvariable=self.units_per_rot) unitsRotationEntry.grid(row=2, column=5, sticky="ew") unitsRotationEntry.configure(state="readonly") Label(topFrame, text="Control Type:", anchor="e").grid(row=2, column=11, sticky="ew") controlMenu = OptionMenu(topFrame, self.control_type, *["Simple", "CTRE", "SparkMax"]) controlMenu.configure(width=25) controlMenu.grid(row=2, column=12, sticky="ew") self.control_type.trace_add("write", getDefaultConfig) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # Body Frame bodyFrame = Frame(self.mainGUI, bd=2, relief="groove") bodyFrame.grid(row=1, column=0, sticky="ew") genProjButton = Button(bodyFrame, text="Generate Project", command=genProject) genProjButton.grid(row=1, column=0, sticky="ew") deployButton = Button(bodyFrame, text="Deploy Project", command=threadedDeploy) deployButton.grid(row=2, column=0, sticky="ew") loggerButton = Button(bodyFrame, text="Launch Data Logger", command=runLogger) loggerButton.grid(row=3, column=0, sticky="ew") analyzerButton = Button(bodyFrame, text="Launch Data Analyzer", command=runAnalyzer) analyzerButton.grid(row=4, column=0, sticky="ew") configEditPane = TextExtension(bodyFrame, textvariable=self.config) configEditPane.grid(row=0, column=1, rowspan=30, columnspan=10) for child in bodyFrame.winfo_children(): child.grid_configure(padx=1, pady=1)
def configure_gui(STATE, RUNNER): def getFile(): file_path = tkinter.filedialog.asksaveasfilename( parent=STATE.mainGUI, title="Choose the data file (.JSON)", initialdir=os.getcwd(), defaultextension=".json", filetypes=(("JSON", "*.json"), ), ) fileEntry.configure(state="normal") fileEntry.delete(0, END) fileEntry.insert(0, file_path) fileEntry.configure(state="readonly") def save(): if STATE.timestamp_enabled.get(): name, ext = os.path.splitext(STATE.file_path.get()) filename = name + time.strftime("%Y%m%d-%H%M") + ext with open(filename, "w") as fp: json.dump(RUNNER.stored_data, fp, indent=4, separators=(",", ": ")) def connect(): if STATE.connect_handle: STATE.mainGUI.after_cancel(STATE.connect_handle) if STATE.team_number.get() != 0: NetworkTables.startClientTeam(STATE.team_number.get()) else: NetworkTables.initialize(server="localhost") NetworkTables.addConnectionListener(RUNNER.connectionListener, immediateNotify=True) NetworkTables.addEntryListener(RUNNER.valueChanged) STATE.connected.set("Connecting...") waitForConnection() def waitForConnection(): if RUNNER.get_nowait() == "connected": STATE.connected.set("Connected") enableTestButtons() else: STATE.connect_handle = STATE.mainGUI.after(10, waitForConnection) def disableTestButtons(): quasiForwardButton.configure(state="disabled") quasiBackwardButton.configure(state="disabled") dynamicForwardButton.configure(state="disabled") dynamicBackwardButton.configure(state="disabled") saveButton.configure(state="disabled") def enableTestButtons(): quasiForwardButton.configure(state="normal") quasiBackwardButton.configure(state="normal") dynamicForwardButton.configure(state="normal") dynamicBackwardButton.configure(state="normal") if (STATE.sf_completed.get() == "Completed" and STATE.sb_completed.get() == "Completed" and STATE.ff_completed.get() == "Completed" and STATE.fb_completed.get() == "Completed"): saveButton.configure(state="normal") def finishTest(textEntry): textEntry.set("Completed") enableTestButtons() def runPostedTasks(): while STATE.runTask(): pass STATE.task_handle = STATE.mainGUI.after(10, runPostedTasks) def quasiForward(): disableTestButtons() STATE.sf_completed.set("Running...") threading.Thread( target=RUNNER.runTest, args=( "slow-forward", 0, STATE.quasi_ramp_rate.get(), lambda: finishTest(STATE.sf_completed), ), ).start() def quasiBackward(): disableTestButtons() STATE.sb_completed.set("Running...") threading.Thread( target=RUNNER.runTest, args=( "slow-backward", 0, -STATE.quasi_ramp_rate.get(), lambda: finishTest(STATE.sb_completed), ), ).start() def dynamicForward(): disableTestButtons() STATE.ff_completed.set("Running...") threading.Thread( target=RUNNER.runTest, args=( "fast-forward", STATE.dynamic_step_voltage.get(), 0, lambda: finishTest(STATE.ff_completed), ), ).start() def dynamicBackward(): disableTestButtons() STATE.fb_completed.set("Running...") threading.Thread( target=RUNNER.runTest, args=( "fast-backward", -STATE.dynamic_step_voltage.get(), 0, lambda: finishTest(STATE.fb_completed), ), ).start() # TOP OF WINDOW (FILE SELECTION) topFrame = Frame(STATE.mainGUI) topFrame.grid(row=0, column=0) Button(topFrame, text="Select Save Location/Name", command=getFile).grid(row=0, column=0, sticky="ew") fileEntry = Entry(topFrame, textvariable=STATE.file_path, width=80) fileEntry.grid(row=0, column=1, columnspan=10) fileEntry.configure(state="readonly") saveButton = Button(topFrame, text="Save Data", command=save, state="disabled") saveButton.grid(row=1, column=0, sticky="ew") Label(topFrame, text="Add Timestamp:", anchor="e").grid(row=1, column=1, sticky="ew") timestampEnabled = Checkbutton(topFrame, variable=STATE.timestamp_enabled) timestampEnabled.grid(row=1, column=2) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # WINDOW BODY (TEST RUNNING CONTROLS) bodyFrame = Frame(STATE.mainGUI, bd=2, relief="groove") bodyFrame.grid(row=1, column=0, sticky="ew") connectButton = Button(bodyFrame, text="Connect to Robot", command=connect) connectButton.grid(row=0, column=0, sticky="ew") connected = Entry(bodyFrame, textvariable=STATE.connected) connected.configure(state="readonly") connected.grid(row=0, column=1, sticky="ew") Label(bodyFrame, text="Team Number:", anchor="e").grid(row=0, column=2, sticky="ew") teamNumEntry = IntEntry(bodyFrame, textvariable=STATE.team_number, width=6) teamNumEntry.grid(row=0, column=3, sticky="ew") Label(bodyFrame, text="Quasistatic ramp rate (V/s):", anchor="e").grid(row=1, column=2, sticky="ew") rampEntry = FloatEntry(bodyFrame, textvariable=STATE.quasi_ramp_rate) rampEntry.grid(row=1, column=3, sticky="ew") Label(bodyFrame, text="Dynamic step voltage (V):", anchor="e").grid(row=3, column=2, sticky="ew") stepEntry = FloatEntry(bodyFrame, textvariable=STATE.dynamic_step_voltage) stepEntry.grid(row=3, column=3, sticky="ew") quasiForwardButton = Button(bodyFrame, text="Quasistatic Forward", command=quasiForward, state="disabled") quasiForwardButton.grid(row=1, column=0, sticky="ew") quasiForwardCompleted = Entry(bodyFrame, textvariable=STATE.sf_completed) quasiForwardCompleted.configure(state="readonly") quasiForwardCompleted.grid(row=1, column=1) quasiBackwardButton = Button(bodyFrame, text="Quasistatic Backward", command=quasiBackward, state="disabled") quasiBackwardButton.grid(row=2, column=0, sticky="ew") quasiBackwardCompleted = Entry(bodyFrame, textvariable=STATE.sb_completed) quasiBackwardCompleted.configure(state="readonly") quasiBackwardCompleted.grid(row=2, column=1) dynamicForwardButton = Button(bodyFrame, text="Dynamic Forward", command=dynamicForward, state="disabled") dynamicForwardButton.grid(row=3, column=0, sticky="ew") dynamicForwardCompleted = Entry(bodyFrame, textvariable=STATE.ff_completed) dynamicForwardCompleted.configure(state="readonly") dynamicForwardCompleted.grid(row=3, column=1) dynamicBackwardButton = Button(bodyFrame, text="Dynamic Backward", command=dynamicBackward, state="disabled") dynamicBackwardButton.grid(row=4, column=0, sticky="ew") dynamicBackwardCompleted = Entry(bodyFrame, textvariable=STATE.fb_completed) dynamicBackwardCompleted.configure(state="readonly") dynamicBackwardCompleted.grid(row=4, column=1) for child in bodyFrame.winfo_children(): child.grid_configure(padx=1, pady=1) runPostedTasks()
def configure_gui(STATE): def getFile(): dataFile = tkinter.filedialog.askopenfile( parent=STATE.mainGUI, mode="rb", title="Choose the data file (.JSON)", initialdir=STATE.project_path.get(), ) fileEntry.configure(state="normal") fileEntry.delete(0, END) fileEntry.insert(0, dataFile.name) fileEntry.configure(state="readonly") try: data = json.load(dataFile) try: # Transform the data into a numpy array to make it easier to use # -> transpose it so we can deal with it in columns for k in JSON_DATA_KEYS: data[k] = np.array(data[k]).transpose() if len(data[JSON_DATA_KEYS[-1]]) > len(columns): messagebox.showerror( "Error!", "You cannot import characterization data from a different mechanism.", ) return STATE.stored_data = data analyzeButton.configure(state="normal") except Exception as e: messagebox.showerror( "Error!", "The structure of the data JSON was not recognized.\n" + "Details\n" + repr(e), ) return except Exception as e: messagebox.showerror( "Error!", "The JSON file could not be loaded.\n" + "Details:\n" + repr(e), parent=STATE.mainGUI, ) return def runAnalysis(): ( STATE.quasi_forward, STATE.quasi_backward, STATE.step_forward, STATE.step_backward, ) = prepare_data(STATE.stored_data, window=STATE.window_size.get(), STATE=STATE) if (STATE.quasi_forward is None or STATE.quasi_backward is None or STATE.step_forward is None or STATE.step_backward is None): return if STATE.subset.get() == "Forward": ks, kv, ka, rsquare = calcFit(STATE.quasi_forward, STATE.step_forward) elif STATE.subset.get() == "Backward": ks, kv, ka, rsquare = calcFit(STATE.quasi_backward, STATE.step_backward) STATE.ks.set(float("%.3g" % ks)) STATE.kv.set(float("%.3g" % kv)) STATE.ka.set(float("%.3g" % ka)) STATE.r_square.set(float("%.3g" % rsquare)) calcGains() timePlotsButton.configure(state="normal") voltPlotsButton.configure(state="normal") fancyPlotButton.configure(state="normal") calcGainsButton.configure(state="normal") def plotTimeDomain(): if STATE.subset.get() == "Forward": _plotTimeDomain("Forward", STATE.quasi_forward, STATE.step_forward) elif STATE.subset.get() == "Backward": _plotTimeDomain("Backward", STATE.quasi_backward, STATE.step_backward) def plotVoltageDomain(): if STATE.subset.get() == "Forward": _plotVoltageDomain("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.subset.get() == "Backward": _plotVoltageDomain("Backward", STATE.quasi_backward, STATE.step_backward, STATE) def plot3D(): if STATE.subset.get() == "Forward": _plot3D("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.subset.get() == "Backward": _plot3D("Backward", STATE.quasi_backward, STATE.step_backward, STATE) def calcGains(): period = (STATE.period.get() if not STATE.has_slave.get() else STATE.slave_period.get()) if STATE.loop_type.get() == "Position": kp, kd = _calcGainsPos( STATE.kv.get(), STATE.ka.get(), STATE.qp.get(), STATE.qv.get(), STATE.max_effort.get(), period, ) else: kp, kd = _calcGainsVel( STATE.kv.get(), STATE.ka.get(), STATE.qv.get(), STATE.max_effort.get(), period, ) # Scale gains to output kp = kp / 12 * STATE.max_controller_output.get() kd = kd / 12 * STATE.max_controller_output.get() # Rescale kD if not time-normalized if not STATE.controller_time_normalized.get(): kd = kd / STATE.period.get() # Get the correct conversion factor for rotations if STATE.units.get() == "Radians": rotation = 2 * math.pi elif STATE.units.get() == "Rotations": rotation = 1 elif STATE.units.get() == "Degrees": rotation = 360 # Convert to controller-native units if STATE.controller_type.get() == "Talon": kp = kp * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) kd = kd * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) if STATE.loop_type.get() == "Velocity": kp = kp * 10 STATE.kp.set(float("%.3g" % kp)) STATE.kd.set(float("%.3g" % kd)) def presetGains(*args): presets = { "Default": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), ), "WPILib (2020-)": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), ), "WPILib (Pre-2020)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.05), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Onboard"), ), "Talon (2020-)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), ), "Talon (Pre-2020)": lambda: ( STATE.max_controller_output.set(1023), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Talon"), ), "Spark MAX": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), ), } presets.get(STATE.gain_units_preset.get(), "Default")() def enableOffboard(*args): if STATE.controller_type.get() == "Onboard": gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="disabled") slavePeriodEntry.configure(state="disabled") elif STATE.controller_type.get() == "Talon": gearingEntry.configure(state="normal") eprEntry.configure(state="normal") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") else: gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") def enableErrorBounds(*args): if STATE.loop_type.get() == "Position": qPEntry.configure(state="normal") else: qPEntry.configure(state="disabled") # TOP OF WINDOW (FILE SELECTION) topFrame = Frame(STATE.mainGUI) topFrame.grid(row=0, column=0, columnspan=4) Button(topFrame, text="Select Data File", command=getFile).grid(row=0, column=0, padx=4) fileEntry = Entry(topFrame, width=80) fileEntry.grid(row=0, column=1, columnspan=3) fileEntry.configure(state="readonly") # The only current option is rotations # This made the implementation of everything easier Label(topFrame, text="Units:", width=10).grid(row=0, column=4) unitChoices = {"Rotations", "Degrees", "Radians"} unitsMenu = OptionMenu(topFrame, STATE.units, *sorted(unitChoices)) unitsMenu.configure(width=10) unitsMenu.grid(row=0, column=5, sticky="ew") Label(topFrame, text="Subset:", width=15).grid(row=0, column=6) subsets = { "Forward", "Backward", } dirMenu = OptionMenu(topFrame, STATE.subset, *sorted(subsets)) dirMenu.configure(width=20) dirMenu.grid(row=0, column=7) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDFORWARD ANALYSIS FRAME ffFrame = Frame(STATE.mainGUI, bd=2, relief="groove") ffFrame.grid(row=1, column=0, columnspan=3, sticky="ns") Label(ffFrame, text="Feedforward Analysis").grid(row=0, column=0, columnspan=5) analyzeButton = Button(ffFrame, text="Analyze Data", command=runAnalysis, state="disabled") analyzeButton.grid(row=1, column=0, sticky="ew") timePlotsButton = Button( ffFrame, text="Time-Domain Diagnostics", command=plotTimeDomain, state="disabled", ) timePlotsButton.grid(row=2, column=0, sticky="ew") voltPlotsButton = Button( ffFrame, text="Voltage-Domain Diagnostics", command=plotVoltageDomain, state="disabled", ) voltPlotsButton.grid(row=3, column=0, sticky="ew") fancyPlotButton = Button(ffFrame, text="3D Diagnostics", command=plot3D, state="disabled") fancyPlotButton.grid(row=4, column=0, sticky="ew") Label(ffFrame, text="Accel Window Size:", anchor="e").grid(row=1, column=1, sticky="ew") windowEntry = IntEntry(ffFrame, textvariable=STATE.window_size, width=5) windowEntry.grid(row=1, column=2) Label(ffFrame, text="Motion Threshold (units/s):", anchor="e").grid(row=2, column=1, sticky="ew") thresholdEntry = FloatEntry(ffFrame, textvariable=STATE.motion_threshold, width=5) thresholdEntry.grid(row=2, column=2) Label(ffFrame, text="kS:", anchor="e").grid(row=1, column=3, sticky="ew") kSEntry = FloatEntry(ffFrame, textvariable=STATE.ks, width=10) kSEntry.grid(row=1, column=4) kSEntry.configure(state="readonly") Label(ffFrame, text="kV:", anchor="e").grid(row=2, column=3, sticky="ew") kVEntry = FloatEntry(ffFrame, textvariable=STATE.kv, width=10) kVEntry.grid(row=2, column=4) kVEntry.configure(state="readonly") Label(ffFrame, text="kA:", anchor="e").grid(row=3, column=3, sticky="ew") kAEntry = FloatEntry(ffFrame, textvariable=STATE.ka, width=10) kAEntry.grid(row=3, column=4) kAEntry.configure(state="readonly") Label(ffFrame, text="r-squared:", anchor="e").grid(row=4, column=3, sticky="ew") rSquareEntry = FloatEntry(ffFrame, textvariable=STATE.r_square, width=10) rSquareEntry.grid(row=4, column=4) rSquareEntry.configure(state="readonly") for child in ffFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDBACK ANALYSIS FRAME fbFrame = Frame(STATE.mainGUI, bd=2, relief="groove") fbFrame.grid(row=1, column=3, columnspan=5) Label(fbFrame, text="Feedback Analysis").grid(row=0, column=0, columnspan=5) Label(fbFrame, text="Gain Settings Preset:", anchor="e").grid(row=1, column=0, sticky="ew") presetChoices = { "Default", "WPILib (2020-)", "WPILib (Pre-2020)", "Talon (2020-)", "Talon (Pre-2020)", "Spark MAX", } presetMenu = OptionMenu(fbFrame, STATE.gain_units_preset, *sorted(presetChoices)) presetMenu.grid(row=1, column=1) presetMenu.config(width=12) STATE.gain_units_preset.trace_add("write", presetGains) Label(fbFrame, text="Controller Period (s):", anchor="e").grid(row=2, column=0, sticky="ew") periodEntry = FloatEntry(fbFrame, textvariable=STATE.period, width=10) periodEntry.grid(row=2, column=1) Label(fbFrame, text="Max Controller Output:", anchor="e").grid(row=3, column=0, sticky="ew") controllerMaxEntry = FloatEntry(fbFrame, textvariable=STATE.max_controller_output, width=10) controllerMaxEntry.grid(row=3, column=1) Label(fbFrame, text="Time-Normalized Controller:", anchor="e").grid(row=4, column=0, sticky="ew") normalizedButton = Checkbutton(fbFrame, variable=STATE.controller_time_normalized) normalizedButton.grid(row=4, column=1) Label(fbFrame, text="Controller Type:", anchor="e").grid(row=5, column=0, sticky="ew") controllerTypes = {"Onboard", "Talon", "Spark"} controllerTypeMenu = OptionMenu(fbFrame, STATE.controller_type, *sorted(controllerTypes)) controllerTypeMenu.grid(row=5, column=1) STATE.controller_type.trace_add("write", enableOffboard) Label(fbFrame, text="Post-Encoder Gearing:", anchor="e").grid(row=6, column=0, sticky="ew") gearingEntry = FloatEntry(fbFrame, textvariable=STATE.gearing, width=10) gearingEntry.configure(state="disabled") gearingEntry.grid(row=6, column=1) Label(fbFrame, text="Encoder EPR:", anchor="e").grid(row=7, column=0, sticky="ew") eprEntry = IntEntry(fbFrame, textvariable=STATE.encoder_epr, width=10) eprEntry.configure(state="disabled") eprEntry.grid(row=7, column=1) Label(fbFrame, text="Has Slave:", anchor="e").grid(row=8, column=0, sticky="ew") hasSlave = Checkbutton(fbFrame, variable=STATE.has_slave) hasSlave.grid(row=8, column=1) hasSlave.configure(state="disabled") STATE.has_slave.trace_add("write", enableOffboard) Label(fbFrame, text="Slave Update Period (s):", anchor="e").grid(row=9, column=0, sticky="ew") slavePeriodEntry = FloatEntry(fbFrame, textvariable=STATE.slave_period, width=10) slavePeriodEntry.grid(row=9, column=1) slavePeriodEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Position Error (units):", anchor="e").grid(row=1, column=2, columnspan=2, sticky="ew") qPEntry = FloatEntry(fbFrame, textvariable=STATE.qp, width=10) qPEntry.grid(row=1, column=4) qPEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Velocity Error (units/s):", anchor="e").grid(row=2, column=2, columnspan=2, sticky="ew") qVEntry = FloatEntry(fbFrame, textvariable=STATE.qv, width=10) qVEntry.grid(row=2, column=4) Label(fbFrame, text="Max Acceptable Control Effort (V):", anchor="e").grid(row=3, column=2, columnspan=2, sticky="ew") effortEntry = FloatEntry(fbFrame, textvariable=STATE.max_effort, width=10) effortEntry.grid(row=3, column=4) Label(fbFrame, text="Loop Type:", anchor="e").grid(row=4, column=2, columnspan=2, sticky="ew") loopTypes = {"Position", "Velocity"} loopTypeMenu = OptionMenu(fbFrame, STATE.loop_type, *sorted(loopTypes)) loopTypeMenu.configure(width=8) loopTypeMenu.grid(row=4, column=4) STATE.loop_type.trace_add("write", enableErrorBounds) Label(fbFrame, text="kV:", anchor="e").grid(row=5, column=2, sticky="ew") kVFBEntry = FloatEntry(fbFrame, textvariable=STATE.kv, width=10) kVFBEntry.grid(row=5, column=3) Label(fbFrame, text="kA:", anchor="e").grid(row=6, column=2, sticky="ew") kAFBEntry = FloatEntry(fbFrame, textvariable=STATE.ka, width=10) kAFBEntry.grid(row=6, column=3) calcGainsButton = Button( fbFrame, text="Calculate Optimal Controller Gains", command=calcGains, state="disabled", ) calcGainsButton.grid(row=7, column=2, columnspan=3) Label(fbFrame, text="kP:", anchor="e").grid(row=8, column=2, sticky="ew") kPEntry = FloatEntry(fbFrame, textvariable=STATE.kp, width=10, state="readonly").grid(row=8, column=3) Label(fbFrame, text="kD:", anchor="e").grid(row=9, column=2, sticky="ew") kDEntry = FloatEntry(fbFrame, textvariable=STATE.kd, width=10, state="readonly").grid(row=9, column=3) for child in fbFrame.winfo_children(): child.grid_configure(padx=1, pady=1)
def configure_gui(STATE): def getFile(): dataFile = tkinter.filedialog.askopenfile( parent=STATE.mainGUI, mode="rb", title="Choose the data file (.JSON)", initialdir=STATE.project_path.get(), ) fileEntry.configure(state="normal") fileEntry.delete(0, END) fileEntry.insert(0, dataFile.name) fileEntry.configure(state="readonly") try: data = json.load(dataFile) try: # Transform the data into a numpy array to make it easier to use # -> transpose it so we can deal with it in columns for k in JSON_DATA_KEYS: data[k] = np.array(data[k]).transpose() if len(data[JSON_DATA_KEYS[-1]]) > len(columns): messagebox.showerror( "Error!", "You cannot import characterization data from a different mechanism.", ) return STATE.stored_data = data analyzeButton.configure(state="normal") except Exception as e: messagebox.showerror( "Error!", "The structure of the data JSON was not recognized.\n" + "Details\n" + repr(e), ) return except Exception as e: messagebox.showerror( "Error!", "The JSON file could not be loaded.\n" + "Details:\n" + repr(e), parent=STATE.mainGUI, ) return def runAnalysis(): ( STATE.quasi_forward, STATE.quasi_backward, STATE.step_forward, STATE.step_backward, ) = prepare_data(STATE.stored_data, window=STATE.window_size.get(), STATE=STATE) if (STATE.quasi_forward is None or STATE.quasi_backward is None or STATE.step_forward is None or STATE.step_backward is None): return if STATE.subset.get() == "Forward": ks, kv, ka, rsquare = calcFit(STATE.quasi_forward, STATE.step_forward) elif STATE.subset.get() == "Backward": ks, kv, ka, rsquare = calcFit(STATE.quasi_backward, STATE.step_backward) STATE.ks.set(float("%.3g" % ks)) STATE.kv.set(float("%.3g" % kv)) STATE.ka.set(float("%.3g" % ka)) STATE.r_square.set(float("%.3g" % rsquare)) calcGains() timePlotsButton.configure(state="normal") voltPlotsButton.configure(state="normal") fancyPlotButton.configure(state="normal") calcGainsButton.configure(state="normal") def plotTimeDomain(): if STATE.subset.get() == "Forward": _plotTimeDomain("Forward", STATE.quasi_forward, STATE.step_forward) elif STATE.subset.get() == "Backward": _plotTimeDomain("Backward", STATE.quasi_backward, STATE.step_backward) def plotVoltageDomain(): if STATE.subset.get() == "Forward": _plotVoltageDomain("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.subset.get() == "Backward": _plotVoltageDomain("Backward", STATE.quasi_backward, STATE.step_backward, STATE) def plot3D(): if STATE.subset.get() == "Forward": _plot3D("Forward", STATE.quasi_forward, STATE.step_forward, STATE) elif STATE.subset.get() == "Backward": _plot3D("Backward", STATE.quasi_backward, STATE.step_backward, STATE) def calcGains(): period = (STATE.period.get() if not STATE.has_slave.get() else STATE.slave_period.get()) if STATE.loop_type.get() == "Position": kp, kd = _calcGainsPos( STATE.kv.get(), STATE.ka.get(), STATE.qp.get(), STATE.qv.get(), STATE.max_effort.get(), period, STATE.measurement_delay.get(), ) else: kp, kd = _calcGainsVel( STATE.kv.get(), STATE.ka.get(), STATE.qv.get(), STATE.max_effort.get(), period, STATE.measurement_delay.get(), ) # Scale gains to output kp = kp / 12 * STATE.max_controller_output.get() kd = kd / 12 * STATE.max_controller_output.get() # Rescale kD if not time-normalized if not STATE.controller_time_normalized.get(): kd = kd / STATE.period.get() # Get the correct conversion factor for rotations if STATE.units.get() == "Radians": rotation = 2 * math.pi elif STATE.units.get() == "Rotations": rotation = 1 elif STATE.units.get() == "Degrees": rotation = 360 # Convert to controller-native units if STATE.controller_type.get() == "Talon": kp = kp * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) kd = kd * rotation / (STATE.encoder_epr.get() * STATE.gearing.get()) if STATE.loop_type.get() == "Velocity": kp = kp * 10 STATE.kp.set(float("%.3g" % kp)) STATE.kd.set(float("%.3g" % kd)) def presetGains(*args): def setMeasurementDelay(delay): STATE.measurement_delay.set(0 if STATE.loop_type.get() == "Position" else delay) # A number of motor controllers use moving average filters; these are types of FIR filters. # A moving average filter with a window size of N is a FIR filter with N taps. # The average delay (in taps) of an arbitrary FIR filter with N taps is (N-1)/2. # All of the delays below assume that 1 T takes 1 ms. # # Proof: # N taps with delays of 0 .. N - 1 T # # average delay = (sum 0 .. N - 1) / N T # = (sum 1 .. N - 1) / N T # # note: sum 1 .. n = n(n + 1) / 2 # # = (N - 1)((N - 1) + 1) / (2N) T # = (N - 1)N / (2N) T # = (N - 1)/2 T presets = { "Default": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), setMeasurementDelay(0), ), "WPILib (2020-)": lambda: ( STATE.max_controller_output.set(12), STATE.period.set(0.02), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Onboard"), # Note that the user will need to remember to set this if the onboard controller is getting delayed measurements. setMeasurementDelay(0), ), "WPILib (Pre-2020)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.05), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Onboard"), # Note that the user will need to remember to set this if the onboard controller is getting delayed measurements. setMeasurementDelay(0), ), "Talon FX": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), # https://phoenix-documentation.readthedocs.io/en/latest/ch14_MCSensor.html#changing-velocity-measurement-parameters # 100 ms sampling period + a moving average window size of 64 (i.e. a 64-tap FIR) = 100/2 ms + (64-1)/2 ms = 81.5 ms. # See above for more info on moving average delays. setMeasurementDelay(81.5), ), "Talon SRX (2020-)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(True), STATE.controller_type.set("Talon"), # https://phoenix-documentation.readthedocs.io/en/latest/ch14_MCSensor.html#changing-velocity-measurement-parameters # 100 ms sampling period + a moving average window size of 64 (i.e. a 64-tap FIR) = 100/2 ms + (64-1)/2 ms = 81.5 ms. # See above for more info on moving average delays. setMeasurementDelay(81.5), ), "Talon SRX (Pre-2020)": lambda: ( STATE.max_controller_output.set(1023), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Talon"), # https://phoenix-documentation.readthedocs.io/en/latest/ch14_MCSensor.html#changing-velocity-measurement-parameters # 100 ms sampling period + a moving average window size of 64 (i.e. a 64-tap FIR) = 100/2 ms + (64-1)/2 ms = 81.5 ms. # See above for more info on moving average delays. setMeasurementDelay(81.5), ), "Spark MAX (brushless)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), # According to a Rev employee on the FRC Discord the window size is 40 so delay = (40-1)/2 ms = 19.5 ms. # See above for more info on moving average delays. setMeasurementDelay(19.5), ), "Spark MAX (brushed)": lambda: ( STATE.max_controller_output.set(1), STATE.period.set(0.001), STATE.controller_time_normalized.set(False), STATE.controller_type.set("Spark"), # https://www.revrobotics.com/content/sw/max/sw-docs/cpp/classrev_1_1_c_a_n_encoder.html#a7e6ce792bc0c0558fb944771df572e6a # 64-tap FIR = (64-1)/2 ms = 31.5 ms delay. # See above for more info on moving average delays. setMeasurementDelay(31.5), ), } presets.get(STATE.gain_units_preset.get(), "Default")() def enableOffboard(*args): if STATE.controller_type.get() == "Onboard": gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="disabled") slavePeriodEntry.configure(state="disabled") elif STATE.controller_type.get() == "Talon": gearingEntry.configure(state="normal") eprEntry.configure(state="normal") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") else: gearingEntry.configure(state="disabled") eprEntry.configure(state="disabled") hasSlave.configure(state="normal") if STATE.has_slave.get(): slavePeriodEntry.configure(state="normal") else: slavePeriodEntry.configure(state="disabled") def enableErrorBounds(*args): if STATE.loop_type.get() == "Position": qPEntry.configure(state="normal") else: qPEntry.configure(state="disabled") # TOP OF WINDOW (FILE SELECTION) topFrame = Frame(STATE.mainGUI) topFrame.grid(row=0, column=0, columnspan=4) Button(topFrame, text="Select Data File", command=getFile).grid(row=0, column=0, padx=4) fileEntry = Entry(topFrame, width=80) fileEntry.grid(row=0, column=1, columnspan=3) fileEntry.configure(state="readonly") # The only current option is rotations # This made the implementation of everything easier Label(topFrame, text="Units:", width=10).grid(row=0, column=4) unitChoices = {"Rotations", "Degrees", "Radians"} unitsMenu = OptionMenu(topFrame, STATE.units, *sorted(unitChoices)) unitsMenu.configure(width=10) unitsMenu.grid(row=0, column=5, sticky="ew") Label(topFrame, text="Subset:", width=15).grid(row=0, column=6) subsets = { "Forward", "Backward", } dirMenu = OptionMenu(topFrame, STATE.subset, *sorted(subsets)) dirMenu.configure(width=20) dirMenu.grid(row=0, column=7) for child in topFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDFORWARD ANALYSIS FRAME ffFrame = Frame(STATE.mainGUI, bd=2, relief="groove") ffFrame.grid(row=1, column=0, columnspan=3, sticky="ns") Label(ffFrame, text="Feedforward Analysis").grid(row=0, column=0, columnspan=5) analyzeButton = Button(ffFrame, text="Analyze Data", command=runAnalysis, state="disabled") analyzeButton.grid(row=1, column=0, sticky="ew") timePlotsButton = Button( ffFrame, text="Time-Domain Diagnostics", command=plotTimeDomain, state="disabled", ) timePlotsButton.grid(row=2, column=0, sticky="ew") voltPlotsButton = Button( ffFrame, text="Voltage-Domain Diagnostics", command=plotVoltageDomain, state="disabled", ) voltPlotsButton.grid(row=3, column=0, sticky="ew") fancyPlotButton = Button(ffFrame, text="3D Diagnostics", command=plot3D, state="disabled") fancyPlotButton.grid(row=4, column=0, sticky="ew") Label(ffFrame, text="Accel Window Size:", anchor="e").grid(row=1, column=1, sticky="ew") windowEntry = IntEntry(ffFrame, textvariable=STATE.window_size, width=5) windowEntry.grid(row=1, column=2) Label(ffFrame, text="Motion Threshold (units/s):", anchor="e").grid(row=2, column=1, sticky="ew") thresholdEntry = FloatEntry(ffFrame, textvariable=STATE.motion_threshold, width=5) thresholdEntry.grid(row=2, column=2) Label(ffFrame, text="kS:", anchor="e").grid(row=1, column=3, sticky="ew") kSEntry = FloatEntry(ffFrame, textvariable=STATE.ks, width=10) kSEntry.grid(row=1, column=4) kSEntry.configure(state="readonly") Label(ffFrame, text="kV:", anchor="e").grid(row=2, column=3, sticky="ew") kVEntry = FloatEntry(ffFrame, textvariable=STATE.kv, width=10) kVEntry.grid(row=2, column=4) kVEntry.configure(state="readonly") Label(ffFrame, text="kA:", anchor="e").grid(row=3, column=3, sticky="ew") kAEntry = FloatEntry(ffFrame, textvariable=STATE.ka, width=10) kAEntry.grid(row=3, column=4) kAEntry.configure(state="readonly") Label(ffFrame, text="r-squared:", anchor="e").grid(row=4, column=3, sticky="ew") rSquareEntry = FloatEntry(ffFrame, textvariable=STATE.r_square, width=10) rSquareEntry.grid(row=4, column=4) rSquareEntry.configure(state="readonly") for child in ffFrame.winfo_children(): child.grid_configure(padx=1, pady=1) # FEEDBACK ANALYSIS FRAME fbFrame = Frame(STATE.mainGUI, bd=2, relief="groove") fbFrame.grid(row=1, column=3, columnspan=5) Label(fbFrame, text="Feedback Analysis").grid(row=0, column=0, columnspan=5) Label(fbFrame, text="Gain Settings Preset:", anchor="e").grid(row=1, column=0, sticky="ew") presetChoices = { "Default", "WPILib (2020-)", "WPILib (Pre-2020)", "Talon FX", "Talon SRX (2020-)", "Talon SRX (Pre-2020)", "Spark MAX (brushless)", "Spark MAX (brushed)", } presetMenu = OptionMenu(fbFrame, STATE.gain_units_preset, *sorted(presetChoices)) presetMenu.grid(row=1, column=1) presetMenu.config(width=12) STATE.gain_units_preset.trace_add("write", presetGains) Label(fbFrame, text="Controller Period (s):", anchor="e").grid(row=2, column=0, sticky="ew") periodEntry = FloatEntry(fbFrame, textvariable=STATE.period, width=10) periodEntry.grid(row=2, column=1) Label(fbFrame, text="Max Controller Output:", anchor="e").grid(row=3, column=0, sticky="ew") controllerMaxEntry = FloatEntry(fbFrame, textvariable=STATE.max_controller_output, width=10) controllerMaxEntry.grid(row=3, column=1) Label(fbFrame, text="Time-Normalized Controller:", anchor="e").grid(row=4, column=0, sticky="ew") normalizedButton = Checkbutton(fbFrame, variable=STATE.controller_time_normalized) normalizedButton.grid(row=4, column=1) Label(fbFrame, text="Controller Type:", anchor="e").grid(row=5, column=0, sticky="ew") controllerTypes = {"Onboard", "Talon", "Spark"} controllerTypeMenu = OptionMenu(fbFrame, STATE.controller_type, *sorted(controllerTypes)) controllerTypeMenu.grid(row=5, column=1) STATE.controller_type.trace_add("write", enableOffboard) Label(fbFrame, text="Measurement delay (ms):", anchor="e").grid(row=6, column=0, sticky="ew") velocityDelay = FloatEntry(fbFrame, textvariable=STATE.measurement_delay, width=10) velocityDelay.grid(row=6, column=1) Label(fbFrame, text="Post-Encoder Gearing:", anchor="e").grid(row=7, column=0, sticky="ew") gearingEntry = FloatEntry(fbFrame, textvariable=STATE.gearing, width=10) gearingEntry.configure(state="disabled") gearingEntry.grid(row=7, column=1) Label(fbFrame, text="Encoder EPR:", anchor="e").grid(row=8, column=0, sticky="ew") eprEntry = IntEntry(fbFrame, textvariable=STATE.encoder_epr, width=10) eprEntry.configure(state="disabled") eprEntry.grid(row=8, column=1) Label(fbFrame, text="Has Slave:", anchor="e").grid(row=9, column=0, sticky="ew") hasSlave = Checkbutton(fbFrame, variable=STATE.has_slave) hasSlave.grid(row=9, column=1) hasSlave.configure(state="disabled") STATE.has_slave.trace_add("write", enableOffboard) Label(fbFrame, text="Slave Update Period (s):", anchor="e").grid(row=10, column=0, sticky="ew") slavePeriodEntry = FloatEntry(fbFrame, textvariable=STATE.slave_period, width=10) slavePeriodEntry.grid(row=10, column=1) slavePeriodEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Position Error (units):", anchor="e").grid(row=1, column=2, columnspan=2, sticky="ew") qPEntry = FloatEntry(fbFrame, textvariable=STATE.qp, width=10) qPEntry.grid(row=1, column=4) qPEntry.configure(state="disabled") Label(fbFrame, text="Max Acceptable Velocity Error (units/s):", anchor="e").grid(row=2, column=2, columnspan=2, sticky="ew") qVEntry = FloatEntry(fbFrame, textvariable=STATE.qv, width=10) qVEntry.grid(row=2, column=4) Label(fbFrame, text="Max Acceptable Control Effort (V):", anchor="e").grid(row=3, column=2, columnspan=2, sticky="ew") effortEntry = FloatEntry(fbFrame, textvariable=STATE.max_effort, width=10) effortEntry.grid(row=3, column=4) Label(fbFrame, text="Loop Type:", anchor="e").grid(row=4, column=2, columnspan=2, sticky="ew") loopTypes = {"Position", "Velocity"} loopTypeMenu = OptionMenu(fbFrame, STATE.loop_type, *sorted(loopTypes)) loopTypeMenu.configure(width=8) loopTypeMenu.grid(row=4, column=4) STATE.loop_type.trace_add("write", enableErrorBounds) # We reset everything to the selected preset when the user changes the loop type # This prevents people from forgetting to change measurement delays STATE.loop_type.trace_add("write", presetGains) Label(fbFrame, text="kV:", anchor="e").grid(row=5, column=2, sticky="ew") kVFBEntry = FloatEntry(fbFrame, textvariable=STATE.kv, width=10) kVFBEntry.grid(row=5, column=3) Label(fbFrame, text="kA:", anchor="e").grid(row=6, column=2, sticky="ew") kAFBEntry = FloatEntry(fbFrame, textvariable=STATE.ka, width=10) kAFBEntry.grid(row=6, column=3) calcGainsButton = Button( fbFrame, text="Calculate Optimal Controller Gains", command=calcGains, state="disabled", ) calcGainsButton.grid(row=7, column=2, columnspan=3) Label(fbFrame, text="kP:", anchor="e").grid(row=8, column=2, sticky="ew") kPEntry = FloatEntry(fbFrame, textvariable=STATE.kp, width=10, state="readonly").grid(row=8, column=3) Label(fbFrame, text="kD:", anchor="e").grid(row=9, column=2, sticky="ew") kDEntry = FloatEntry(fbFrame, textvariable=STATE.kd, width=10, state="readonly").grid(row=9, column=3) for child in fbFrame.winfo_children(): child.grid_configure(padx=1, pady=1)