Laelaps  2.3.5
RoadNarrows Robotics Small Outdoor Mobile Robot Project
laelaps_tune_motors.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 
3 ###############################################################################
4 #
5 # Package: RoadNarrows Robotics Laelaps Robotic Mobile Platform Package
6 #
7 # Script: laelaps_motors
8 #
9 # File: laelaps_motors
10 #
11 ## \file
12 ##
13 ## $LastChangedDate$
14 ## $Rev$
15 ##
16 ## \brief Graphical interface to Laelaps motors for tuning and debugging.
17 ##
18 ## \author Robin Knight (robin.knight@roadnarrows.com)
19 ##
20 ## \par Copyright
21 ## \h_copy 2015-2017. RoadNarrows LLC.\n
22 ## http://www.roadnarrows.com\n
23 ## All Rights Reserved
24 ##
25 # @EulaBegin@
26 #
27 # Unless otherwise stated explicitly, all materials contained are copyrighted
28 # and may not be used without RoadNarrows LLC's written consent,
29 # except as provided in these terms and conditions or in the copyright
30 # notice (documents and software) or other proprietary notice provided with
31 # the relevant materials.
32 #
33 # IN NO EVENT SHALL THE AUTHOR, ROADNARROWS LLC, OR ANY
34 # MEMBERS/EMPLOYEES/CONTRACTORS OF ROADNARROWS OR DISTRIBUTORS OF THIS SOFTWARE
35 # BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR
36 # CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
37 # DOCUMENTATION, EVEN IF THE AUTHORS OR ANY OF THE ABOVE PARTIES HAVE BEEN
38 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 #
40 # THE AUTHORS AND ROADNARROWS LLC SPECIFICALLY DISCLAIM ANY WARRANTIES,
41 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
42 # FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN
43 # "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO
44 # PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
45 #
46 # @EulaEnd@
47 #
48 ###############################################################################
49 
50 import sys
51 import os
52 import time
53 import math
54 import getopt
55 
56 if sys.version_info[0] < 3:
57  from Tkinter import *
58  from Tkconstants import *
59  from tkFileDialog import *
60  import tkFont
61 else:
62  from tkinter import *
63  from Tkconstants import *
64  from tkFileDialog import *
65  import tkFont
66 
67 import webbrowser
68 
69 import Laelaps.RoboClaw as Roboclaw
70 import Laelaps.SysConf as SysConf
71 import Laelaps.Utils as Utils
72 import Laelaps.VelPlot as VelPlot
73 import Laelaps.PidParams as PidParams
74 
75 
76 # ------------------------------------------------------------------------------
77 # Globals
78 # ------------------------------------------------------------------------------
79 
80 ## \brief Application version. Update as needed.
81 AppVersion = '1.0.0'
82 
83 ## \brief Laelaps wiki URL
84 LaelapsWikiUrl = "https://github.com/roadnarrows-robotics/laelaps/wiki"
85 
86 ## \brief Image search paths.
87 ImagePath = [
88  "/prj/share/Laelaps/images",
89  "/usr/local/share/Laelaps/images",
90  "/prj/share/appkit/images",
91  "/usr/local/share/appkit/images"
92 ]
93 
94 ## \brief Common UI colors.
95 UIColors = {
96  'normal': 'black',
97  'ok': '#008800',
98  'focus': '#0000aa',
99  'warning': '#aa6600',
100  'error': '#cc0000',
101  'left_front': '#aa0000',
102  'right_front': '#aa6600',
103  'left_rear': '#0066aa',
104  'right_rear': '#0000aa',
105 }
106 
107 # \brief Tri-state values.
108 TriState = {
109  'none': -1,
110  'off': 0,
111  'on': 1
112 }
113 
114 _ctlrkey = lambda pos: str.lower(pos) + "_ctlr"
115 _motkey = lambda pos: str.lower(pos) + "_motor"
116 
117 #
118 ## \brief Concatenate two dictionaries to make a third.
119 #
120 def dictConcat(d1, d2):
121  e = {}
122  e.update(d1)
123  e.update(d2)
124  return e
125 
126 
127 # ------------------------------------------------------------------------------
128 # Class window
129 # ------------------------------------------------------------------------------
130 
131 ##
132 ## \brief Window class supporting application.
133 ##
134 class window(Frame):
135 
136  #
137  ## \brief Constructor.
138  ##
139  ## \param master Window parent master widget.
140  ## \param cnf Configuration dictionary.
141  ## \param kw Keyword options.
142  #
143  def __init__(self, master=None, cnf={}, **kw):
144  self.m_wMaster = master
145 
146  self.perfStart()
147 
148  # intialize window data
149  kw = self.initData(kw)
150 
151  # initialize parent object frame
152  Frame.__init__(self, master=master, cnf=cnf, **kw)
153 
154  # window title
155  self.master.title("Laelaps Motors")
156 
157  self.m_imageLoader = Utils.ImageLoader(py_pkg='Laelaps.images',
158  image_paths=ImagePath)
159 
160  self.m_icons['app_icon'] = self.m_imageLoader.load("icons/LaelapsIcon.png")
161  #RDK self.master.tk.call('wm', 'iconphoto', self.master._w,
162  #RDK self.m_icons['app_icon'])
163 
164  #print 'DBG', self.m_var
165 
166  # create and show widgets
167  self.createWidgets()
168  self.grid(row=0, column=0, padx=5, pady=5)
169 
170  self.update_idletasks()
171  #width = self.winfo_width()
172  #print 'DBG: self.width =', width
173 
174  # RDK Comment out when done. Debug
175  self.dbgSeedFields()
176 
177  def perfStart(self):
178  self.t0 = time.time()
179 
180  def perfMark(self, msg):
181  t1 = time.time()
182  print "%.4f: %s" % (t1 - self.t0, msg)
183  self.t0 = t1
184 
185  #
186  ## \brief Initialize class state data.
187  ##
188  ## Any keywords for this application specific window that are not supported
189  ## by the Frame Tkinter class must be removed.
190  ##
191  ## \param kw Keyword options.
192  ##
193  ## \return Modified keywords sans this specific class.
194  #
195  def initData(self, kw):
196  self.m_botName = "laelaps"
197  #RDK self.m_cfgPanel = PanelConfigDlg.ConfigDft.copy() # panel config
198  self.m_icons = {} # must keep loaded icons referenced
199  self.m_wBttn = {} # button widgets
200  self.m_plotVel = None # no plot yet
201 
202  # fixed names, addresses, positions
203  self.m_motorCtlrPos = ['Front', 'Rear']
204  self.m_motorCtlrAddr = {'Front': SysConf.MotorCtlrAddrFront,
205  'Rear': SysConf.MotorCtlrAddrRear}
206  self.m_motorPos = ['Left', 'Right']
207  self.m_powertrain = {
208  'front_ctlr': {'left_motor': 'left_front', 'right_motor': 'right_front'},
209  'rear_ctlr': {'left_motor': 'left_rear', 'right_motor': 'right_rear'}
210  }
211 
212  # variable db
213  self.m_var = {
214  'front_ctlr': {'changes': False, 'left_motor': {}, 'right_motor': {}},
215  'rear_ctlr': {'changes': False, 'left_motor': {}, 'right_motor': {}}
216  }
217 
218  # override panel configuration
219  #RDK if kw.has_key('config'):
220  #RDK self.m_cfgPanel = kw['config'].copy()
221  #RDK del kw['config']
222  #RDK # or read configuration
223  #RDK else:
224  #RDK self.readPanelConfig()
225 
226  return kw
227 
228  def dbgSeedFields(self):
229  for ctlrpos in self.m_motorCtlrPos:
230  ctlrkey = _ctlrkey(ctlrpos)
231  addr = self.m_motorCtlrAddr[ctlrpos]
232 
233  self.m_var[ctlrkey]['addr'].set("0x%02x" % (addr))
234  self.m_var[ctlrkey]['baudrate'].set(SysConf.MotorCtlrBaudRate)
235  self.m_var[ctlrkey]['model'].set("2x15a")
236  self.m_var[ctlrkey]['version'].set("4.1.6")
237  self.m_var[ctlrkey]['batt_max'].set(33.6)
238  self.m_var[ctlrkey]['batt_min'].set(8.1)
239  self.m_var[ctlrkey]['logic_max'].set(33.6)
240  self.m_var[ctlrkey]['logic_min'].set(5.0)
241  self.m_var[ctlrkey]['battery'].set(11.9)
242  self.m_var[ctlrkey]['logic'].set(12.0)
243  self.m_var[ctlrkey]['temp'].set(26.4)
244  self.m_var[ctlrkey]['alarm_temp']['w']['image'] = \
245  self.m_icons['led_green']
246  self.m_var[ctlrkey]['alarm_temp']['val'] = TriState['off']
247  self.m_var[ctlrkey]['alarm_batt_high']['w']['image'] = \
248  self.m_icons['led_red']
249  self.m_var[ctlrkey]['alarm_batt_high']['val'] = TriState['on']
250 
251  for motorpos in self.m_motorPos:
252  motorkey = _motkey(motorpos)
253 
254  self.m_var[ctlrkey][motorkey]['enc_type'].set("Quadrature")
255  self.setPidParams(ctlrkey, motorkey, 1.0, 0.25, 0.5, 100000, 1000.0)
256 
257  #
258  ## \brief Create gui widgets with supporting data and show.
259  #
260  def createWidgets(self):
261  # preload some of the needed images
262  self.m_icons['rn_logo'] = self.m_imageLoader.load("RNLogo48.png");
263  self.m_icons['laelaps_logo'] = \
264  self.m_imageLoader.load("icon_laelaps_logo.png");
265  self.m_icons['led_dark'] = self.m_imageLoader.load("icon_led_dark_16.png")
266  self.m_icons['led_green'] = self.m_imageLoader.load("icon_led_green_16.png")
267  self.m_icons['led_red'] = self.m_imageLoader.load("icon_led_red_16.png")
268  self.m_icons['linked_h'] = \
269  self.m_imageLoader.load('icon_chain_h_linked_16.png')
270  self.m_icons['unlinked_h'] = \
271  self.m_imageLoader.load('icon_chain_h_unlinked_16.png')
272  self.m_icons['linked_v'] = \
273  self.m_imageLoader.load('icon_chain_v_linked_16.png')
274  self.m_icons['unlinked_v'] = \
275  self.m_imageLoader.load('icon_chain_v_unlinked_16.png')
276  self.perfMark("Preloaded images")
277 
278  self.createMenu()
279  self.update_idletasks()
280  self.perfMark("Created menus")
281 
282  self.createHeading()
283  self.update_idletasks()
284  self.perfMark("Created heading")
285 
286  self.createLeftButtons()
287  self.update_idletasks()
288  self.perfMark("Created left buttons")
289 
290  self.createCenterPanel()
291  self.update_idletasks()
292  self.perfMark("Created center panel")
293 
294  self.createStatusBar()
295  self.update_idletasks()
296  self.perfMark("Created status bar")
297 
298  #RDK self.updateButtonState(self.m_keysFewMoves, 'disabled')
299 
300  #
301  ## \brief Create menu
302  #
303  def createMenu(self):
304  # top menu bar
305  self.m_wMenuBar = Menu(self)
306 
307  # file menu
308  self.m_wMenuFile = Menu(self.m_wMenuBar, tearoff=0)
309  self.m_wMenuFile.add_command(label="Save...", command=self.notimpl)
310  self.m_wMenuFile.add_separator()
311  self.m_wMenuFile.add_command(label="Quit", command=self.destroy)
312  self.m_wMenuBar.add_cascade(label="File", menu=self.m_wMenuFile)
313 
314  # move menu
315  self.m_wMenuMove = Menu(self.m_wMenuBar, tearoff=0)
316  self.m_wMenuBar.add_cascade(label="Move", menu=self.m_wMenuMove)
317 
318  # help menu
319  self.m_wMenuHelp = Menu(self.m_wMenuBar, tearoff=0)
320  self.m_wMenuHelp.add_command(label="Online Laelaps Wiki",
321  command=lambda aurl=LaelapsWikiUrl:webbrowser.open_new(LaelapsWikiUrl))
322  self.m_wMenuHelp.add_separator()
323  self.m_wMenuHelp.add_command(label="About...", command=self.about)
324  self.m_wMenuBar.add_cascade(label="Help", menu=self.m_wMenuHelp)
325 
326  self.m_wMaster.config(menu=self.m_wMenuBar)
327 
328  #
329  ## \brief Create top gui heading.
330  #
331  def createHeading(self):
332  # rn logo
333  w = Label(self)
334  if self.m_icons['rn_logo']:
335  w['image'] = self.m_icons['rn_logo']
336  w['anchor'] = W
337  else:
338  w['text'] = 'rn'
339  w['anchor'] = W
340  w['width'] = 5
341  w.grid(row=0, column=0, sticky=W)
342 
343  # top heading
344  w = Label(self)
345  w['font'] = ('Helvetica', 16)
346  w['text'] = "Laelaps Motors"
347  w['anchor'] = CENTER
348  w['justify'] = CENTER
349  w.grid(row=0, column=1, sticky=E+W)
350  self.m_wTopHeading = w
351 
352  # hek logo
353  w = Label(self)
354  if self.m_icons['laelaps_logo']:
355  w['image'] = self.m_icons['laelaps_logo']
356  w['anchor'] = E
357  else:
358  w['text'] = 'mot'
359  w['anchor'] = E
360  w['width'] = 5
361  w.grid(row=0, column=2, sticky=E)
362 
363  #
364  ## \brief Create gui left hand side buttons.
365  #
366  def createLeftButtons(self):
367  wframe = Frame(self)
368  wframe['borderwidth'] = 0
369  wframe['relief'] = 'ridge'
370  wframe.grid(row=1, column=0, padx=1, pady=20, sticky=N+W+E)
371 
372  row = 0
373 
374  # apply
375  row += 1
376  w = self.createButton(wframe, "Apply", "icons/icon_check.png",
377  self.apply)
378  w.grid(row=row, column=0, sticky=N+E+W)
379 
380  # stop
381  row += 1
382  w = self.createButton(wframe, "Stop",
383  "icons/icon_stop2.png", self.stop)
384  w.grid(row=row, column=0, sticky=N+E+W)
385 
386  # save
387  row += 1
388  w = self.createButton(wframe, "Save", "icons/icon_floppy.png",
389  self.save)
390  w.grid(row=row, column=0, sticky=N+E+W)
391 
392  # info
393  row += 1
394  w = self.createButton(wframe, "About",
395  "icons/icon_info.png", self.about)
396  w.grid(row=row, column=0, sticky=N+E+W)
397 
398  # quit
399  row += 1
400  w = self.createButton(wframe, "Quit", "icons/icon_exit.png", self.destroy,
401  fg='red')
402  w.grid(row=row, column=0, sticky=N+E+W)
403 
404  #
405  ## \brief Create robot status and joint state center panel.
406  #
407  def createCenterPanel(self):
408  wframe = Frame(self)
409  wframe['borderwidth'] = 0
410  wframe['relief'] = 'ridge'
411  wframe.grid(row=1, column=1, columnspan=2,
412  padx=1, pady=3, sticky=N+W+E)
413 
414  self.createStatusPanel(wframe, 0, 0)
415  self.perfMark("Created status panel")
416 
417  self.createVelTuningPanel(wframe, 0, 1)
418  self.perfMark("Created velocity tuning panel")
419 
420  self.update_idletasks()
421  width = wframe.winfo_width()
422 
423  self.createPlotPanel(wframe, 1, 0, width)
424  self.perfMark("Created plot panel")
425 
426  #
427  ## \brief Create motor status panel.
428  ##
429  ## \param parent Parent widget.
430  ## \param row Row in parent widget.
431  ## \param col Column in parent widget.
432  #
433  def createStatusPanel(self, parent, row, col):
434  wframe = Frame(parent)
435  wframe['relief'] = 'flat'
436  wframe.grid(row=row, column=col, padx=1, pady=3, sticky=N+W+E)
437 
438  row = 0
439  col = 0
440 
441  for name in self.m_motorCtlrPos:
442  self.createMotorCtlrPanel(wframe, row, col, name)
443  row += 1
444 
445  #
446  ## \brief Create motor controller panel.
447  ##
448  ## \param parent Parent widget.
449  ## \param row Row in parent widget.
450  ## \param col Column in parent widget.
451  ## \param ctlrpos Motor controller position string.
452  #
453  def createMotorCtlrPanel(self, parent, row, col, ctlrpos):
454  wframe = LabelFrame(parent)
455  wframe['text'] = "%s Motor Controller State" % (ctlrpos)
456  wframe['font'] = ('Helvetica', 12)
457  wframe['fg'] = UIColors['focus']
458  wframe['borderwidth'] = 2
459  wframe['relief'] = 'ridge'
460  wframe.grid(row=row, column=col, padx=1, pady=1, sticky=N+W+E)
461 
462  addr = self.m_motorCtlrAddr[ctlrpos]
463 
464  ctlrkey = _ctlrkey(ctlrpos)
465 
466  # common widget and grid options
467  wcfgLabel = {'anchor': E, 'justify': RIGHT}
468  gcfgLabel = {'padx': (5,2), 'pady': 2, 'sticky': E}
469  wcfgValue = {'anchor': W, 'justify': LEFT, 'relief': 'solid'}
470  gcfgValue = {'padx': (0,2), 'pady': 1, 'sticky': W}
471 
472  col = 0
473 
474  # packet address label
475  self.makeWidget(wframe, Label,
476  dictConcat({'text': 'Address:'}, wcfgLabel),
477  dictConcat({'row': 0, 'column': col}, gcfgLabel))
478 
479  # packet address
480  self.m_var[ctlrkey]['addr'] = StringVar();
481  self.makeWidget(wframe, Label,
482  dictConcat({'textvariable': self.m_var[ctlrkey]['addr'], 'width': 8},
483  wcfgValue),
484  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
485 
486  # baudrate label
487  self.makeWidget(wframe, Label,
488  dictConcat({'text': 'Baudrate:'}, wcfgLabel),
489  dictConcat({'row': 1, 'column': col}, gcfgLabel))
490 
491  # baudrate
492  self.m_var[ctlrkey]['baudrate'] = IntVar()
493  self.makeWidget(wframe, Label,
494  dictConcat({'textvariable': self.m_var[ctlrkey]['baudrate'],
495  'width': 8}, wcfgValue),
496  dictConcat({'row': 1, 'column': col+1}, gcfgValue))
497 
498  col += 2
499 
500  # motor controller model label
501  self.makeWidget(wframe, Label,
502  dictConcat({'text': 'Model:'}, wcfgLabel),
503  dictConcat({'row': 0, 'column': col}, gcfgLabel))
504 
505  # motor controller model
506  self.m_var[ctlrkey]['model'] = StringVar()
507  self.makeWidget(wframe, Label,
508  dictConcat({'textvariable': self.m_var[ctlrkey]['model'], 'width': 10},
509  wcfgValue),
510  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
511 
512  # firmware version label
513  self.makeWidget(wframe, Label,
514  dictConcat({'text': 'Version:'}, wcfgLabel),
515  dictConcat({'row': 1, 'column': col}, gcfgLabel))
516 
517  # firmware version
518  self.m_var[ctlrkey]['version'] = StringVar()
519  self.makeWidget(wframe, Label,
520  dictConcat({'textvariable': self.m_var[ctlrkey]['version'],
521  'width': 10}, wcfgValue),
522  dictConcat({'row': 1, 'column': col+1}, gcfgValue))
523 
524  col += 2
525 
526  # maximum battery voltage label
527  self.makeWidget(wframe, Label,
528  dictConcat({'text': 'Max Battery (V):'}, wcfgLabel),
529  dictConcat({'row': 0, 'column': col}, gcfgLabel))
530 
531  # maximum battery voltage
532  self.m_var[ctlrkey]['batt_max'] = DoubleVar()
533  self.makeWidget(wframe, Label,
534  dictConcat({'textvariable': self.m_var[ctlrkey]['batt_max'],
535  'width': 5}, wcfgValue),
536  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
537 
538  # minimum battery voltage label
539  self.makeWidget(wframe, Label,
540  dictConcat({'text': 'Min Battery (V):'}, wcfgLabel),
541  dictConcat({'row': 1, 'column': col}, gcfgLabel))
542 
543  # minimum battery voltage
544  self.m_var[ctlrkey]['batt_min'] = DoubleVar()
545  self.makeWidget(wframe, Label,
546  dictConcat({'textvariable': self.m_var[ctlrkey]['batt_min'],
547  'width': 5}, wcfgValue),
548  dictConcat({'row': 1, 'column': col+1}, gcfgValue))
549 
550  col += 2
551 
552  # maximum logic voltage label
553  self.makeWidget(wframe, Label,
554  dictConcat({'text': 'Max Logic (V):'}, wcfgLabel),
555  dictConcat({'row': 0, 'column': col}, gcfgLabel))
556 
557  # maximum logic voltage
558  self.m_var[ctlrkey]['logic_max'] = DoubleVar()
559  self.makeWidget(wframe, Label,
560  dictConcat({'textvariable': self.m_var[ctlrkey]['logic_max'],
561  'width': 5}, wcfgValue),
562  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
563 
564  # minimum logic voltage label
565  self.makeWidget(wframe, Label,
566  dictConcat({'text': 'Min Logic (V):'}, wcfgLabel),
567  dictConcat({'row': 1, 'column': col}, gcfgLabel))
568 
569  # minimum logic voltage
570  self.m_var[ctlrkey]['logic_min'] = DoubleVar()
571  self.makeWidget(wframe, Label,
572  dictConcat({'textvariable': self.m_var[ctlrkey]['logic_min'],
573  'width': 5}, wcfgValue),
574  dictConcat({'row': 1, 'column': col+1}, gcfgValue))
575 
576  col += 2
577 
578  # battery voltage label
579  self.makeWidget(wframe, Label,
580  dictConcat({'text': 'Battery (V):'}, wcfgLabel),
581  dictConcat({'row': 0, 'column': col}, gcfgLabel))
582 
583  # battery voltage
584  self.m_var[ctlrkey]['battery'] = DoubleVar()
585  self.makeWidget(wframe, Label,
586  dictConcat({'textvariable': self.m_var[ctlrkey]['battery'], 'width': 5},
587  wcfgValue),
588  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
589 
590  # logic voltage label
591  self.makeWidget(wframe, Label,
592  dictConcat({'text': 'Logic (V):'}, wcfgLabel),
593  dictConcat({'row': 1, 'column': col}, gcfgLabel))
594 
595  # logic voltage
596  self.m_var[ctlrkey]['logic'] = DoubleVar()
597  self.makeWidget(wframe, Label,
598  dictConcat({'textvariable': self.m_var[ctlrkey]['logic'],
599  'width': 5}, wcfgValue),
600  dictConcat({'row': 1, 'column': col+1}, gcfgValue))
601 
602  col += 2
603 
604  # temperature label
605  self.makeWidget(wframe, Label,
606  dictConcat({'text': 'Temp (C):'}, wcfgLabel),
607  dictConcat({'row': 0, 'column': col}, gcfgLabel))
608 
609  # temperature
610  self.m_var[ctlrkey]['temp'] = DoubleVar()
611  self.makeWidget(wframe, Label,
612  dictConcat({'textvariable': self.m_var[ctlrkey]['temp'],
613  'width': 6}, wcfgValue),
614  dictConcat({'row': 0, 'column': col+1}, gcfgValue))
615 
616  col += 2
617 
618  wframe = Frame(wframe)
619  wframe.grid(row=3, column=0, columnspan=col, padx=1, pady=5, sticky=N+W+E)
620 
621  row = 0
622 
623  for motorpos in self.m_motorPos:
624  self.createMotorPanel(wframe, row, 0, ctlrkey, motorpos)
625  row += 1
626 
627  wframe = Frame(wframe)
628  wframe.grid(row=row, column=0, columnspan=col, padx=1, pady=5, sticky=N+W+E)
629 
630  self.createAlarmsPanel(wframe, 0, 0, ctlrkey)
631 
632  #
633  ## \brief Create motor controller alarm panel.
634  ##
635  ## \param parent Parent widget.
636  ## \param row Row in parent widget.
637  ## \param col Column in parent widget.
638  ## \param ctlrkey Motor controller db key.
639  ##
640  def createAlarmsPanel(self, parent, row, col, ctlrkey):
641  wframe = Frame(parent)
642  wframe['relief'] = 'flat'
643  wframe.grid(row=row, column=col, padx=1, pady=3, sticky=N+W+E)
644 
645  row = 0
646  col = 0
647 
648  self.makeWidget(wframe, Label,
649  {'text': "Alarms:",
650  #'font': ('Helvetica', 10),
651  'fg': UIColors['focus'],
652  'borderwidth': 2},
653  {'row': row, 'column': col, 'padx': 1, 'pady': 2, 'sticky': E})
654 
655  col += 1
656 
657  # Left motor over current alarm
658  self.createAlarmWidget(wframe, row, col, ctlrkey,
659  'Left Motor\nOver Current', 'alarm_lmoc')
660 
661  col += 1
662 
663  # Right motor over current alarm
664  self.createAlarmWidget(wframe, row, col, ctlrkey,
665  'Right Motor\nOver Current', 'alarm_rmoc')
666 
667  col += 1
668 
669  # Battery low voltage alarm
670  self.createAlarmWidget(wframe, row, col, ctlrkey,
671  'Battery\nLow Volt', 'alarm_batt_low')
672 
673  col += 1
674 
675  # Battery high voltage alarm
676  self.createAlarmWidget(wframe, row, col, ctlrkey,
677  'Battery\nHigh Volt', 'alarm_batt_high')
678 
679  col += 1
680 
681  # Logic low voltage alarm
682  self.createAlarmWidget(wframe, row, col, ctlrkey,
683  'Logic\nLow Volt', 'alarm_logic_low')
684 
685  col += 1
686 
687  # Logic high voltage alarm
688  self.createAlarmWidget(wframe, row, col, ctlrkey,
689  'Logic\nHigh Volt', 'alarm_logic_high')
690 
691  col += 1
692 
693  # Temperature alarm
694  self.createAlarmWidget(wframe, row, col, ctlrkey,
695  'Over\nTemperature', 'alarm_temp')
696 
697  col += 1
698 
699  # Emergency stop alarm
700  self.createAlarmWidget(wframe, row, col, ctlrkey,
701  'Emergency\nStopped', 'alarm_estop')
702 
703  #
704  ## \brief Create alarm widgets and initalize alarm db.
705  ##
706  ## \param parent Parent widget.
707  ## \param row Row in parent widget.
708  ## \param col Column in parent widget.
709  ## \param ctlrkey Motor controller db key.
710  ## \param text Alarm label text.
711  ## \param key Alarm db key.
712  #
713  def createAlarmWidget(self, parent, row, col, ctlrkey, text, key):
714  wframe = Frame(parent)
715  wframe['borderwidth'] = 1
716  wframe['relief'] = 'solid'
717  wframe.grid(row=row, column=col, padx=0, pady=1, sticky=N+W+E)
718 
719  row = 0
720  col = 0
721 
722  self.makeWidget(wframe, Label,
723  {'text': text, 'justify': CENTER, 'anchor': CENTER},
724  {'row':row, 'column':col, 'padx':(1, 1), 'pady':2, 'sticky':W+S+E})
725 
726  w = self.makeWidget(wframe, Label,
727  {'image': self.m_icons['led_dark'], 'justify': CENTER, 'anchor': CENTER},
728  {'row':row, 'column':col+1, 'padx':1, 'pady':(2,4), 'sticky':W+S+E})
729 
730  self.m_var[ctlrkey][key] = {'w': w, 'val': TriState['none']}
731 
732 
733  #
734  ## \brief Create motor controller motor panel.
735  ##
736  ## \param parent Parent widget.
737  ## \param row Row in parent widget.
738  ## \param col Column in parent widget.
739  ## \param ctlrkey Motor controller db key.
740  ## \param motorpos Motor position string.
741  #
742  def createMotorPanel(self, parent, row, col, ctlrkey, motorpos):
743  self.makeWidget(parent, Label,
744  {'text': "%s Motor:" % (motorpos),
745  'font': ('Helvetica', 10),
746  'fg': UIColors['focus'],
747  'borderwidth': 2},
748  {'row': row, 'column': col, 'padx': 1, 'pady': 2, 'sticky': N+W+E})
749 
750  motorkey = _motkey(motorpos)
751 
752  # common widget and grid options
753  wcfgLabel = {'anchor': E, 'justify': RIGHT}
754  gcfgLabel = {'padx': (5,2), 'pady': 2, 'sticky': E}
755  wcfgValue = {'anchor': W, 'justify': LEFT, 'relief': 'solid'}
756  gcfgValue = {'padx': (0,2), 'pady': 1, 'sticky': W}
757 
758  col = 1
759 
760  # encoder type label
761  self.makeWidget(parent, Label,
762  dictConcat({'text': 'Encoder Type:'}, wcfgLabel),
763  dictConcat({'row': row, 'column': col}, gcfgLabel))
764 
765  # encoder type
766  self.m_var[ctlrkey][motorkey]['enc_type'] = StringVar()
767  self.makeWidget(parent, Label,
768  dictConcat({'textvariable': self.m_var[ctlrkey][motorkey]['enc_type'],
769  'width': 10}, wcfgValue),
770  dictConcat({'row': row, 'column': col+1}, gcfgValue))
771 
772  col += 2
773 
774  # amps label
775  self.makeWidget(parent, Label,
776  dictConcat({'text': 'Amps:'}, wcfgLabel),
777  dictConcat({'row': row, 'column': col}, gcfgLabel))
778 
779  # amps
780  self.m_var[ctlrkey][motorkey]['amps'] = DoubleVar()
781  self.makeWidget(parent, Label,
782  dictConcat({'textvariable': self.m_var[ctlrkey][motorkey]['amps'],
783  'width': 8}, wcfgValue),
784  dictConcat({'row': row, 'column': col+1}, gcfgValue))
785 
786  col += 2
787 
788  # encoder label
789  self.makeWidget(parent, Label,
790  dictConcat({'text': 'Encoder:'}, wcfgLabel),
791  dictConcat({'row': row, 'column': col}, gcfgLabel))
792 
793  # encoder
794  self.m_var[ctlrkey][motorkey]['encoder'] = IntVar()
795  self.makeWidget(parent, Label,
796  dictConcat({'textvariable': self.m_var[ctlrkey][motorkey]['encoder'],
797  'width': 12}, wcfgValue),
798  dictConcat({'row': row, 'column': col+1}, gcfgValue))
799 
800  col += 2
801 
802  # speed label
803  self.makeWidget(parent, Label,
804  dictConcat({'text': 'Speed (QPPS):'}, wcfgLabel),
805  dictConcat({'row': row, 'column': col}, gcfgLabel))
806 
807  # speed
808  self.m_var[ctlrkey][motorkey]['speed'] = IntVar()
809  self.makeWidget(parent, Label,
810  dictConcat({'textvariable': self.m_var[ctlrkey][motorkey]['speed'],
811  'width': 8}, wcfgValue),
812  dictConcat({'row': row, 'column': col+1}, gcfgValue))
813 
814  col += 2
815 
816  #
817  ## \brief Create velocity tuning panel.
818  ##
819  ## \param parent Parent widget.
820  ## \param row Row in parent widget.
821  ## \param col Column in parent widget.
822  #
823  def createVelTuningPanel(self, parent, row, col):
824  wframe = Frame(parent)
825  wframe['relief'] = 'flat'
826  wframe.grid(row=row, column=col, padx=1, pady=1, sticky=N+W+E)
827 
828  row = 0
829  col = 0
830 
831  for ctlrpos in self.m_motorCtlrPos:
832  self.createMotorsTuningPanel(wframe, row, col, ctlrpos)
833  col += 1
834 
835  #
836  ## \brief Create motor controller motors tuning panel.
837  ##
838  ## \param parent Parent widget.
839  ## \param row Row in parent widget.
840  ## \param col Column in parent widget.
841  ## \param ctlrpos Motor controller position name.
842  #
843  def createMotorsTuningPanel(self, parent, row, col, ctlrpos):
844  wframe = LabelFrame(parent)
845  wframe['text'] = "%s Velocity Tuning" % (ctlrpos)
846  wframe['font'] = ('Helvetica', 12)
847  wframe['fg'] = UIColors['focus']
848  wframe['borderwidth'] = 2
849  wframe['relief'] = 'ridge'
850  wframe.grid(row=row, column=col, padx=1, pady=5, sticky=N+W+E)
851 
852  ctlrkey = _ctlrkey(ctlrpos)
853 
854  wcfgLabel = {'anchor': E, 'justify': RIGHT}
855  gcfgLabel = {'padx': (5,2), 'pady': 2, 'sticky': E}
856  wcfgValue = {'anchor': W, 'justify': LEFT, 'relief': 'solid'}
857  gcfgValue = {'padx': (0,5), 'pady': 1, 'sticky': W}
858 
859  row = 0
860  col = 0
861 
862  # PID subframe
863  self.createPidParamsPanel(wframe, row, col, ctlrkey)
864 
865  # Setpoint subframe
866  row = 1
867  col = 0
868 
869  self.createSetpointsPanel(wframe, row, col, ctlrkey)
870 
871  #
872  ## \brief Create motor controller motors PID parameters panel.
873  ##
874  ## \param parent Parent widget.
875  ## \param row Row in parent widget.
876  ## \param col Column in parent widget.
877  ## \param ctlrkey Motor controller db key.
878  #
879  def createPidParamsPanel(self, parent, row, col, ctlrkey):
880  wframe = LabelFrame(parent)
881  wframe['text'] = 'PID Parameters'
882  #wframe['font'] = ('Helvetica', 10),
883  wframe['fg'] = UIColors['focus'],
884  wframe['borderwidth'] = 2
885  wframe['relief'] = 'ridge'
886  wframe.grid(row=row, column=col, padx=(5,1), pady=(5,0), sticky=N+W+E)
887 
888  wcfgLabel = {'anchor': E, 'justify': RIGHT}
889  gcfgLabel = {'padx': (5,2), 'pady': 2, 'sticky': E}
890  wcfgValue = {'anchor': W, 'justify': LEFT, 'relief': 'solid'}
891  gcfgValue = {'padx': (0,5), 'pady': 1, 'sticky': W}
892 
893  #
894  # PID labels
895  #
896  row = 1
897  col = 0
898 
899  self.makeWidget(wframe, Label,
900  dictConcat({'text': 'P:'}, wcfgLabel),
901  dictConcat({'row': row, 'column': col}, gcfgLabel))
902 
903  row += 1
904 
905  self.makeWidget(wframe, Label,
906  dictConcat({'text': 'I:'}, wcfgLabel),
907  dictConcat({'row': row, 'column': col}, gcfgLabel))
908 
909  row += 1
910 
911  self.makeWidget(wframe, Label,
912  dictConcat({'text': 'D:'}, wcfgLabel),
913  dictConcat({'row': row, 'column': col}, gcfgLabel))
914 
915  row += 1
916 
917  self.makeWidget(wframe, Label,
918  dictConcat({'text': 'Max QPPS:'}, wcfgLabel),
919  dictConcat({'row': row, 'column': col}, gcfgLabel))
920 
921  row += 1
922 
923  self.makeWidget(wframe, Label,
924  dictConcat({'text': 'Max Accel:'}, wcfgLabel),
925  dictConcat({'row': row, 'column': col}, gcfgLabel))
926 
927  col = 1
928 
929  #
930  # Motor PID fields
931  #
932  for motorpos in self.m_motorPos:
933  motorkey = _motkey(motorpos)
934 
935  row = 0
936 
937  self.makeWidget(wframe, Label,
938  {'text': motorpos+' Motor', 'anchor': W, 'justify': LEFT},
939  {'row': row, 'column': col, 'padx': (0,1), 'pady': 2, 'sticky': W})
940 
941  # P
942  row += 1
943  key = 'vel_pid_p'
944 
945  self.m_var[ctlrkey][motorkey][key] = DoubleVar()
946 
947  self.makeWidget(wframe, Entry,
948  {'textvariable': self.m_var[ctlrkey][motorkey][key],
949  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
950  dictConcat({'row': row, 'column': col}, gcfgValue))
951 
952  # I
953  row += 1
954  key = 'vel_pid_i'
955 
956  self.m_var[ctlrkey][motorkey][key] = DoubleVar()
957 
958  self.makeWidget(wframe, Entry,
959  {'textvariable': self.m_var[ctlrkey][motorkey][key],
960  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
961  dictConcat({'row': row, 'column': col}, gcfgValue))
962 
963  # D
964  row += 1
965  key = 'vel_pid_d'
966 
967  self.m_var[ctlrkey][motorkey][key] = DoubleVar()
968 
969  self.makeWidget(wframe, Entry,
970  {'textvariable': self.m_var[ctlrkey][motorkey][key],
971  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
972  dictConcat({'row': row, 'column': col}, gcfgValue))
973 
974  # QPPS
975  row += 1
976  key = 'vel_pid_qpps'
977 
978  self.m_var[ctlrkey][motorkey][key] = DoubleVar()
979 
980  self.makeWidget(wframe, Entry,
981  {'textvariable': self.m_var[ctlrkey][motorkey][key],
982  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
983  dictConcat({'row': row, 'column': col}, gcfgValue))
984 
985  # acceleration
986  row += 1
987  key = 'vel_pid_max_accel'
988 
989  self.m_var[ctlrkey][motorkey][key] = DoubleVar()
990 
991  self.makeWidget(wframe, Entry,
992  {'textvariable': self.m_var[ctlrkey][motorkey][key],
993  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
994  dictConcat({'row': row, 'column': col}, gcfgValue))
995 
996  row += 1
997  col += 1
998 
999  # current or new parameters in controller
1000  self.m_var[ctlrkey][motorkey]['vel_pid_params'] = PidParams.VelPidParams()
1001 
1002  # upwards arrow with left turn
1003  self.makeWidget(wframe, Label,
1004  dictConcat({'text': u'\u21b0'}, wcfgLabel),
1005  {'row': 1, 'column': col, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1006 
1007  # spacer
1008  self.makeWidget(wframe, Label,
1009  dictConcat({'text': ''}, wcfgLabel),
1010  {'row': 2, 'column': col, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1011 
1012  # link/unlink button
1013  def cblink(ck): return lambda: self.cbLinkCtlrPids(ck)
1014  icon_key = 'linked_v'
1015  w = Button(wframe)
1016  if self.m_icons[icon_key]:
1017  w['image'] = self.m_icons[icon_key]
1018  w['padx'] = 0
1019  w['pady'] = 0
1020  w['anchor'] = W
1021  w['width'] = 16
1022  else:
1023  w['text'] = 'link'
1024  w['anchor'] = CENTER
1025  w['width'] = 6
1026  self.m_var[ctlrkey]['pid_linked'] = {'w': w, 'val': True};
1027  w['command'] = cblink(ctlrkey)
1028  w.grid(row=3, column=col)
1029 
1030  # spacer
1031  self.makeWidget(wframe, Label,
1032  dictConcat({'text': ''}, wcfgLabel),
1033  {'row': 4, 'column': col, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1034 
1035  # downward arrow with left turn
1036  self.makeWidget(wframe, Label,
1037  dictConcat({'text': u'\u21b2'}, wcfgLabel),
1038  {'row': 5, 'column': col, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1039 
1040  #
1041  ## \brief Create motor controller motor velocity setpoints panel.
1042  ##
1043  ## \param parent Parent widget.
1044  ## \param row Row in parent widget.
1045  ## \param col Column in parent widget.
1046  ## \param ctlrkey Motor controller db key.
1047  #
1048  def createSetpointsPanel(self, parent, row, col, ctlrkey):
1049  wframe = LabelFrame(parent)
1050  wframe['text'] = 'Setpoints'
1051  #wframe['font'] = ('Helvetica', 10),
1052  wframe['fg'] = UIColors['focus'],
1053  wframe['borderwidth'] = 2
1054  wframe['relief'] = 'ridge'
1055  wframe.grid(row=row, column=col, padx=(5,1), pady=(5,0),
1056  sticky=N+W+E+S)
1057 
1058  wcfgLabel = {'anchor': E, 'justify': RIGHT}
1059  gcfgLabel = {'padx': (5,2), 'pady': 2, 'sticky': E}
1060  wcfgValue = {'anchor': W, 'justify': LEFT, 'relief': 'solid'}
1061  gcfgValue = {'padx': (0,5), 'pady': 1, 'sticky': W}
1062 
1063  row = 0
1064 
1065  def cbspd(ck, mk, fk): return lambda: self.cbVelSetpoint(ck, mk, fk, 0)
1066  def cbpct(ck, mk, fk): return lambda v: self.cbVelSetpoint(ck, mk, fk, v)
1067 
1068  for motorpos in self.m_motorPos:
1069  motorkey = _motkey(motorpos)
1070 
1071  col = 0
1072 
1073  self.makeWidget(wframe, Label,
1074  dictConcat({'text': motorpos+' Motor:'}, wcfgLabel),
1075  {'row': row, 'column': col, 'padx': (2,2), 'pady': (1,0), 'sticky': W})
1076 
1077  col += 1
1078 
1079  key = 'setpoint_speed'
1080 
1081  self.m_var[ctlrkey][motorkey][key] = IntVar()
1082 
1083  self.makeWidget(wframe, Entry,
1084  {'textvariable': self.m_var[ctlrkey][motorkey][key],
1085  'validate': 'focusout',
1086  'validatecommand': cbspd(ctlrkey, motorkey, key),
1087  'width': 8, 'justify': LEFT, 'relief': 'sunken'},
1088  {'row': row, 'column': col,
1089  'padx': (2,2), 'pady': (1,0), 'sticky': E})
1090 
1091  row += 1
1092  col = 0
1093  key = 'setpoint_percent'
1094 
1095  self.m_var[ctlrkey][motorkey][key] = IntVar()
1096 
1097  # slider
1098  self.makeWidget(wframe, Scale,
1099  {'variable': self.m_var[ctlrkey][motorkey][key],
1100  'command': cbpct(ctlrkey, motorkey, key),
1101  'from_': -100, 'to': 100, 'resolution': 1, 'orient': HORIZONTAL,
1102  'tickinterval': 50, 'length': 210},
1103  {'row': row, 'column': col, 'columnspan': 2,
1104  'padx': (2,2), 'pady': (0,0), 'sticky': W})
1105 
1106  row += 1
1107 
1108  # spacer
1109  if motorpos == 'Left':
1110  self.makeWidget(wframe, Label,
1111  dictConcat({'text': ''}, wcfgLabel),
1112  {'row': row, 'column': col, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1113 
1114  row += 1
1115 
1116  # upwards arrow with left turn
1117  self.makeWidget(wframe, Label,
1118  dictConcat({'text': u'\u21b0'}, wcfgLabel),
1119  {'row': 0, 'column': 2, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1120 
1121  # spacer
1122  self.makeWidget(wframe, Label,
1123  dictConcat({'text': ''}, wcfgLabel),
1124  {'row': 1, 'column': 2, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1125 
1126  # link/unlink button
1127  def cblink(ck): return lambda: self.cbLinkCtlrSetpoints(ck)
1128  icon_key = 'linked_v'
1129  w = Button(wframe)
1130  if self.m_icons[icon_key]:
1131  w['image'] = self.m_icons[icon_key]
1132  w['padx'] = 0
1133  w['pady'] = 0
1134  w['anchor'] = W
1135  w['width'] = 16
1136  else:
1137  w['text'] = 'link'
1138  w['anchor'] = CENTER
1139  w['width'] = 6
1140  self.m_var[ctlrkey]['setpoint_linked'] = {'w': w, 'val': True};
1141  w['command'] = cblink(ctlrkey)
1142  w.grid(row=2, column=2)
1143 
1144  # spacer
1145  self.makeWidget(wframe, Label,
1146  dictConcat({'text': ''}, wcfgLabel),
1147  {'row': 3, 'column': 2, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1148 
1149  # downward arrow with left turn
1150  self.makeWidget(wframe, Label,
1151  dictConcat({'text': u'\u21b2'}, wcfgLabel),
1152  {'row': 4, 'column': 2, 'padx': (2,2), 'pady': (1,1), 'sticky': W})
1153 
1154  #
1155  ## \brief Create real-time plot panel.
1156  ##
1157  ## \param parent Parent widget.
1158  ## \param row Row in parent widget.
1159  ## \param col Column in parent widget.
1160  ## \param width Total available width of plot panel in pixels.
1161  #
1162  def createPlotPanel(self, parent, row, col, width):
1163  wframe = LabelFrame(parent)
1164  wframe['text'] = "Real-Time Velocity Plot"
1165  wframe['font'] = ('Helvetica', 12)
1166  wframe['fg'] = UIColors['focus']
1167  wframe['borderwidth'] = 2
1168  wframe['relief'] = 'ridge'
1169  wframe.grid(row=row, column=col, columnspan=2, padx=1, pady=1, sticky=N+W+E)
1170 
1171  subwidth = self.createPlotControls(wframe, 0, 0)
1172  self.perfMark("Created plot controls")
1173 
1174  self.createPlotCanvas(wframe, 0, 1, width-subwidth)
1175  self.perfMark("Created plot canvas")
1176 
1177  #
1178  ## \brief Create real-time plot controls.
1179  ##
1180  ## \param parent Parent widget.
1181  ## \param row Row in parent widget.
1182  ## \param col Column in parent widget.
1183  ##
1184  ## \return Total available width of control frame in pixels.
1185  #
1186  def createPlotControls(self, parent, row, col):
1187  wframe = Frame(parent)
1188  wframe['borderwidth'] = 0
1189  wframe['relief'] = 'flat'
1190  wframe.grid(row=row, column=col, padx=1, pady=1, sticky=N+W+E)
1191 
1192  row = 0
1193  col = 0
1194 
1195  def cben(ck, mk, fk): return lambda: self.cbEnDisVelPlot(ck, mk, fk)
1196 
1197  ctlrkey = "front_ctlr"
1198  motorkey = "left_motor"
1199  fieldkey = "plotvel"
1200 
1201  # left front graph color
1202  self.makeWidget(wframe, Label,
1203  {'text': ' ', 'bg': UIColors['left_front'],
1204  'anchor': W, 'justify': LEFT},
1205  {'row': row, 'column': col, 'padx': (5,1), 'pady': (1,1), 'sticky': W})
1206 
1207  # left front motor checkbutton
1208  self.m_var[ctlrkey][motorkey][fieldkey] = IntVar();
1209 
1210  self.makeWidget(wframe, Checkbutton,
1211  {'text': 'Left Front', 'command': cben(ctlrkey, motorkey, fieldkey),
1212  'variable': self.m_var[ctlrkey][motorkey][fieldkey],
1213  'anchor': W, 'justify': LEFT},
1214  {'row': row, 'column': col+1, 'padx': (1,5), 'pady': (1,1), 'sticky': W})
1215 
1216  row += 1
1217 
1218  motorkey = "right_motor"
1219 
1220  # right front graph color
1221  self.makeWidget(wframe, Label,
1222  {'text': ' ', 'bg': UIColors['right_front'],
1223  'anchor': W, 'justify': LEFT},
1224  {'row': row, 'column': col, 'padx': (5,1), 'pady': (1,1), 'sticky': W})
1225 
1226  # right front motor checkbutton
1227  self.m_var[ctlrkey][motorkey][fieldkey] = IntVar();
1228 
1229  self.makeWidget(wframe, Checkbutton,
1230  {'text': 'Right Front', 'command': cben(ctlrkey, motorkey, fieldkey),
1231  'variable': self.m_var[ctlrkey][motorkey][fieldkey],
1232  'anchor': W, 'justify': LEFT},
1233  {'row': row, 'column': col+1, 'padx': (1,5), 'pady': (1,1), 'sticky': W})
1234 
1235  row += 1
1236 
1237  ctlrkey = "rear_ctlr"
1238  motorkey = "left_motor"
1239 
1240  # left rear graph color
1241  self.makeWidget(wframe, Label,
1242  {'text': ' ', 'bg': UIColors['left_rear'],
1243  'anchor': W, 'justify': LEFT},
1244  {'row': row, 'column': col, 'padx': (5,1), 'pady': (1,1), 'sticky': W})
1245 
1246  # left rear motor checkbutton
1247  self.m_var[ctlrkey][motorkey][fieldkey] = IntVar();
1248 
1249  self.makeWidget(wframe, Checkbutton,
1250  {'text': 'Left Rear', 'command': cben(ctlrkey, motorkey, fieldkey),
1251  'variable': self.m_var[ctlrkey][motorkey][fieldkey],
1252  'anchor': W, 'justify': LEFT},
1253  {'row': row, 'column': col+1, 'padx': (1,5), 'pady': (1,1), 'sticky': W})
1254 
1255  row += 1
1256 
1257  motorkey = "right_motor"
1258 
1259  # right rear graph color
1260  self.makeWidget(wframe, Label,
1261  {'text': ' ', 'bg': UIColors['right_rear'],
1262  'anchor': W, 'justify': LEFT},
1263  {'row': row, 'column': col, 'padx': (5,1), 'pady': (1,1), 'sticky': W})
1264 
1265  # right rear motor checkbutton
1266  self.m_var[ctlrkey][motorkey][fieldkey] = IntVar();
1267 
1268  self.makeWidget(wframe, Checkbutton,
1269  {'text': 'Right Rear', 'command': cben(ctlrkey, motorkey, fieldkey),
1270  'variable': self.m_var[ctlrkey][motorkey][fieldkey],
1271  'anchor': W, 'justify': LEFT},
1272  {'row': row, 'column': col+1, 'padx': (1,5), 'pady': (1,1), 'sticky': W})
1273 
1274  row += 1
1275 
1276  self.update_idletasks()
1277  width = wframe.winfo_width()
1278  #print 'DBG: plotcontrols width =', width
1279 
1280  return width
1281 
1282  #
1283  ## \brief Create real-time plot canvas.
1284  ##
1285  ## \param parent Parent widget.
1286  ## \param row Row in parent widget.
1287  ## \param col Column in parent widget.
1288  ## \param width Total available width of canvas in pixels.
1289  #
1290  def createPlotCanvas(self, parent, row, col, width):
1291  wframe = Frame(parent)
1292  wframe['borderwidth'] = 0
1293  wframe['relief'] = 'ridge'
1294  wframe.grid(row=row, column=col, padx=1, pady=1, sticky=N+W+E)
1295 
1296  # width and height in pixels, subbing out margins
1297  width -= 40
1298  height = 400
1299 
1300  wCanvas = Canvas(wframe, height=height, width=width)
1301  wCanvas.grid(row=0, column=0)
1302 
1303  self.m_plotVel = VelPlot.VelPlot(wCanvas, width, height,
1304  ['left_front', 'right_front', 'left_rear', 'right_rear'],
1305  UIColors)
1306 
1307  self.update_idletasks()
1308 
1309  #
1310  ## \brief Make widgets from widget description table.
1311  ##
1312  ## The description table is a list of wdesc widget description dictionaries.
1313  ## Each wdesc dictionary must have the following keys and values:
1314  ## 'widget': W - W is a Tkinter widget class (e.g. Label, button,
1315  ## listbox, etc).
1316  ## 'wcfg': D - D is a dictionary of widget-specific configuration options.
1317  ## 'gcfg': D - D is a dictionary of grid options.
1318  ##
1319  ## \param parent Parent widget.
1320  ## \param wdescTbl List of [wdesc, wdesc,...].
1321  #
1322  def makeWidgets(self, parent, wdescTbl):
1323  for wdesc in wdescTbl:
1324  self.makeWidget(parent, wdesc['widget'], wdesc['wcfg'], wdesc['gcfg'])
1325 
1326  #
1327  ## \brief Make widget.
1328  ##
1329  ## \param parent Parent widget.
1330  ## \param widget Tkinter widget class.
1331  ## \param wcfg Dictionary of widget-specific configuration options.
1332  ## \param gcfg Dictionary of grid options.
1333  ##
1334  ## \return Returns made widget.
1335  #
1336  def makeWidget(self, parent, widget, wcfg, gcfg):
1337  w = widget(parent, **wcfg)
1338  w.grid(**gcfg)
1339  return w
1340 
1341  #
1342  ## \brief Create gui status bar at bottom of gui window.
1343  #
1344  def createStatusBar(self):
1345  wframe = Frame(self)
1346  wframe['borderwidth'] = 2
1347  wframe['relief'] = 'ridge'
1348  wframe.grid(row=2, column=0, columnspan=3, padx=1, pady=3, sticky=N+E+W+S)
1349 
1350  self.m_varStatus = StringVar()
1351  self.m_varStatus.set("Calibration required.")
1352  self.m_wStatusBar = Entry(wframe)
1353  self.m_wStatusBar['width'] = wframe['width']
1354  self.m_wStatusBar['relief'] = 'flat'
1355  self.m_wStatusBar['textvar'] = self.m_varStatus
1356  self.m_wStatusBar['fg'] = UIColors['normal']
1357  self.m_wStatusBar['state'] = 'readonly'
1358  self.m_wStatusBar.grid(row=0, column=0, padx=3, pady=3, sticky=N+E+W+S)
1359 
1360  #
1361  ## \brief Update button activation states.
1362  #
1363  def updateButtonState(self, keys, state):
1364  for key in keys:
1365  self.m_wBttn[key]['state'] = state
1366 
1367  #
1368  ## \brief Create button.
1369  ##
1370  ## \param parent Parent widget.
1371  ## \param text Button text.
1372  ## \param imagefile Image file name. None for no image.
1373  ## \param command Callback for button push.
1374  ## \param fg Foreground text color.
1375  ##
1376  ## \return Button widget.
1377  #
1378  def createButton(self, parent, text, imagefile, command, fg='black'):
1379  key = str.lower(text.replace("\n", "_"))
1380  self.m_icons[key] = self.m_imageLoader.load(imagefile)
1381  w = Button(parent)
1382  w['text'] = text
1383  if self.m_icons[key]:
1384  w['image'] = self.m_icons[key]
1385  w['compound'] = LEFT
1386  w['padx'] = 0
1387  w['pady'] = 0
1388  w['anchor'] = W
1389  w['width'] = 105
1390  else:
1391  w['anchor'] = CENTER
1392  w['width'] = 10
1393  w['fg'] = fg
1394  w['command'] = command
1395  self.m_wBttn[key] = w
1396  return self.m_wBttn[key]
1397 
1398  #
1399  ## \brief Destroy window callback.
1400  #
1401  def destroy(self):
1402  self.quit()
1403 
1404  #
1405  ## \brief Not implemented callback.
1406  #
1407  def notimpl(self):
1408  emsg = "Window function not implemented yet."
1409  self.showError(emsg)
1410  print emsg
1411 
1412  #
1413  ## \brief Apply tuning tweaks to controllers callback.
1414  #
1415  def apply(self):
1416  pass
1417 
1418  #
1419  ## \brief Save tuning to tuning file and controller EEPROM callback.
1420  #
1421  def save(self):
1422  pass
1423 
1424  #
1425  ## \brief Stop Laelaps
1426  #
1427  def stop(self):
1428  pass
1429 
1430  #
1431  ## \brief Show about dialog callback.
1432  #
1433  def about(self):
1434  prodInfo = self.getProductInfo()
1435  dlg = AboutDlg(master=self, info=prodInfo, app_ver=AppVersion)
1436 
1437  def cbLinkAllPids(self):
1438  pass
1439 
1440  def cbLinkCtlrPids(self, ck):
1441  fk = 'pid_linked'
1442  if self.m_var[ck][fk]['val']:
1443  self.m_var[ck][fk]['w']['image'] = self.m_icons['unlinked_v']
1444  self.m_var[ck][fk]['val'] = False
1445  else:
1446  self.m_var[ck][fk]['w']['image'] = self.m_icons['linked_v']
1447  self.m_var[ck][fk]['val'] = True
1448 
1449  def cbLinkAllSetpoints(self):
1450  pass
1451 
1452  def cbLinkCtlrSetpoints(self, ck):
1453  fk = 'setpoint_linked'
1454  if self.m_var[ck][fk]['val']:
1455  self.m_var[ck][fk]['w']['image'] = self.m_icons['unlinked_v']
1456  self.m_var[ck][fk]['val'] = False
1457  else:
1458  self.m_var[ck][fk]['w']['image'] = self.m_icons['linked_v']
1459  self.m_var[ck][fk]['val'] = True
1460 
1461  #
1462  ## \brief Setpoint changed callback.
1463  ##
1464  ## \param ck Controller key.
1465  ## \param mk Motor key.
1466  ## \param fk Field key.
1467  ## \param v Value (ignored).
1468  ##
1469  ## \return True
1470  #
1471  def cbVelSetpoint(self, ck, mk, fk, v):
1472  if self.m_plotVel is None:
1473  return True
1474  val = self.m_var[ck][mk][fk].get()
1475  pk = self.m_powertrain[ck][mk]
1476  self.m_plotVel.setpoint(pk, val)
1477  return True
1478 
1479  #
1480  ## \brief Enable/disable velocity plot.
1481  ##
1482  ## \param ck Controller key.
1483  ## \param mk Motor key.
1484  ## \param fk Field key.
1485  #
1486  def cbEnDisVelPlot(self, ck, mk, fk):
1487  if self.m_plotVel is None:
1488  return
1489  name = self.m_powertrain[ck][mk]
1490  val = self.m_var[ck][mk][fk].get()
1491  sp = self.m_var[ck][mk]['setpoint_speed'].get()
1492  if val:
1493  self.m_plotVel.enable(name, sp)
1494  else:
1495  self.m_plotVel.disable(name)
1496 
1497  def setPidParams(self, ck, mk, Kp, Ki, Kd, maxQpps, maxAccel):
1498  # UI fields
1499  self.m_var[ck][mk]['vel_pid_p'].set(Kp)
1500  self.m_var[ck][mk]['vel_pid_i'].set(Ki)
1501  self.m_var[ck][mk]['vel_pid_d'].set(Kd)
1502  self.m_var[ck][mk]['vel_pid_qpps'].set(maxQpps)
1503  self.m_var[ck][mk]['vel_pid_max_accel'].set(maxAccel)
1504 
1505  # new/current PID parameters
1506  self.m_var[ck][mk]['vel_pid_params'].m_Kp = Kp
1507  self.m_var[ck][mk]['vel_pid_params'].m_Ki = Ki
1508  self.m_var[ck][mk]['vel_pid_params'].m_Kd = Kd
1509  self.m_var[ck][mk]['vel_pid_params'].m_maxQpps = maxQpps
1510  self.m_var[ck][mk]['vel_pid_params'].m_maxAccel = maxAccel
1511 
1512  #
1513  ## \brief Get product information.
1514  ##
1515  ## \return Returns product information on success, None on failure.
1516  #
1517  def getProductInfo(self):
1518  prodInfo = None
1519  try:
1520  rospy.wait_for_service("laelaps_control/get_product_info", timeout=5)
1521  except rospy.ROSException, e:
1522  self.showError('Get product info: ' + e.message + '.')
1523  else:
1524  try:
1525  get_product_info = rospy.ServiceProxy(
1526  'laelaps_control/get_product_info',
1527  GetProductInfo)
1528  rsp = get_product_info()
1529  prodInfo = rsp.i
1530  except rospy.ServiceException, e:
1531  self.showError("Get product info request failed: %s." % (e.message))
1532  return prodInfo
1533 
1534  #
1535  ## \brief Get Laelaps name.
1536  ##
1537  ## \return Returns name.
1538  #
1539  def getLaelapsName(self):
1540  name = "laelaps"
1541  try:
1542  rospy.wait_for_service("laelaps_control/get_product_info", timeout=5)
1543  except rospy.ROSException, e:
1544  rospy.logerr("Get product info: %s." % (e.message))
1545  else:
1546  try:
1547  get_product_info = rospy.ServiceProxy(
1548  'laelaps_control/get_product_info',
1549  GetProductInfo)
1550  rsp = get_product_info()
1551  name = rsp.i.hostname
1552  except rospy.ServiceException, e:
1553  rospy.logerr("Get product info request failed: %s." % (e.message))
1554  return name
1555 
1556  #
1557  ## \brief Update motor status.
1558  ##
1559  #
1560  def updateStatus(self):
1561  pass
1562 
1563  #
1564  ## \brief Show information message on status bar.
1565  ##
1566  ## \param msg Info message string.
1567  #
1568  def showInfo(self, msg):
1569  self.m_wStatusBar["state"] = "normal"
1570  self.m_wStatusBar["fg"] = UIColors['normal']
1571  self.m_varStatus.set(msg)
1572  self.m_wStatusBar["state"] = "readonly"
1573 
1574  #
1575  ## \brief Show error message on status bar.
1576  ##
1577  ## \param msg Error message string.
1578  #
1579  def showError(self, msg):
1580  self.m_wStatusBar["state"] = "normal"
1581  self.m_wStatusBar["fg"] = UIColors['error']
1582  self.m_varStatus.set(msg)
1583  self.m_wStatusBar["state"] = "readonly"
1584 
1585  #
1586  ## \brief Show text on read-only entry.
1587  ##
1588  ## \param w Entry widget.
1589  ## \param var Bound entry variable.
1590  ## \param val Variable value.
1591  ## \param fg Text foreground color.
1592  #
1593  def showEntry(self, w, var, val, fg='black'):
1594  w['state'] = 'normal'
1595  w['fg'] = fg
1596  var.set(val)
1597  w['state'] = 'readonly'
1598 
1599  #
1600  ## \brief Map alignment value to justify equivalent.
1601  ##
1602  ## \param align Alignment.
1603  ##
1604  ## \return Tk justify.
1605  #
1606  def alignToJustify(self, align):
1607  if align == W:
1608  return LEFT
1609  elif align == E:
1610  return RIGHT
1611  else:
1612  return CENTER
1613 
1614  #
1615  ## \brief Final window initializations.
1616  ##
1617  ## Both the window data and widgets, along with ROS node application, have
1618  ## been fully initialized.
1619  #
1620  def finalInits(self):
1621  self.m_botName = self.getLaelapsName()
1622  self.master.title("Laelaps Control Panel - %s" % (self.m_botName))
1623  self.m_wTopHeading['text'] = "Laelaps Control Panel - %s" % \
1624  (self.m_botName)
1625 
1626 
1627 # ------------------------------------------------------------------------------
1628 # Exception Class usage
1629 # ------------------------------------------------------------------------------
1630 
1631 ##
1632 ## \brief Unit test command-line exception class.
1633 ##
1634 ## Raise usage excpetion.
1635 ##
1636 class usage(Exception):
1637 
1638  ##
1639  ## \brief Constructor.
1640  ##
1641  ## \param msg Error message string.
1642  ##
1643  def __init__(self, msg):
1644  ## error message attribute
1645  self.msg = msg
1646 
1647 
1648 # ------------------------------------------------------------------------------
1649 # Class application
1650 # ------------------------------------------------------------------------------
1651 
1652 ##
1653 ## \brief Laelaps control panel.
1654 ##
1655 class application():
1656 
1657  #
1658  ## \brief Constructor.
1659  #
1660  def __init__(self):
1661  self._Argv0 = os.path.basename(__file__)
1662  self.m_win = None
1663 
1664  #
1665  ## \brief Print usage error.
1666  ##
1667  ## \param emsg Error message string.
1668  #
1669  def printUsageErr(self, emsg):
1670  if emsg:
1671  print "%s: %s" % (self._Argv0, emsg)
1672  else:
1673  print "%s: error" % (self._Argv0)
1674  print "Try '%s --help' for more information." % (self._Argv0)
1675 
1676  ## \brief Print Command-Line Usage Message.
1677  def printUsage(self):
1678  print \
1679 """
1680 usage: %s [OPTIONS]
1681  %s --help
1682 
1683 Options and arguments:
1684 -h, --help : Display this help and exit.
1685 """ % (self._Argv0, self._Argv0)
1686 
1687  #
1688  ## \brief Get command-line options
1689  ##
1690  ## \param argv Argument list. If not None, then overrides
1691  ## command-line arguments.
1692  ## \param [out] kwargs Keyword argument list.
1693  ##
1694  ## \return Parsed keyword arguments.
1695  #
1696  def getOptions(self, argv=None, **kwargs):
1697  if argv is None:
1698  argv = sys.argv
1699 
1700  self._Argv0 = os.path.basename(kwargs.get('argv0', __file__))
1701 
1702  # defaults
1703  kwargs['debug'] = False
1704 
1705  # parse command-line options
1706  try:
1707  opts, args = getopt.getopt(argv[1:], "?h",
1708  ['help', ''])
1709  except getopt.error, msg:
1710  raise usage(msg)
1711  for opt, optarg in opts:
1712  if opt in ('-h', '--help', '-?'):
1713  self.printUsage()
1714  sys.exit(0)
1715 
1716  #if len(args) < 1:
1717  # self.printUsageErr("No input xml file specified")
1718  # sys.exit(2)
1719  #else:
1720  # kwargs['filename'] = args[0]
1721 
1722  return kwargs
1723 
1724  #
1725  ## \brief Initialize interface to hek_robot.
1726  #
1727  def initRobot(self):
1728  self.m_win.showInfo("Initializing interface to Laelaps.")
1729 
1730  self.m_win.showInfo("Laelaps motor interface initialized.")
1731 
1732  #
1733  ## \brief Run application.
1734  ##
1735  ## \param argv Optional argument list to override command-line arguments.
1736  ## \param kwargs Optional keyword argument list.
1737  ##
1738  ## \return Exit code.
1739  #
1740  def run(self, argv=None, **kwargs):
1741 
1742  # parse command-line options and arguments
1743  try:
1744  kwargs = self.getOptions(argv, **kwargs)
1745  except usage, e:
1746  print e.msg
1747  return 2
1748 
1749  # create root
1750  root = Tk()
1751 
1752  # create application window
1753  self.m_win = window(master=root)
1754 
1755  # destroy window on 'x'
1756  root.protocol('WM_DELETE_WINDOW', root.destroy)
1757 
1758  root.columnconfigure(0, weight=1)
1759  root.rowconfigure(0, weight=1)
1760 
1761  # initialize robot interface
1762  self.initRobot()
1763 
1764  # go for it
1765  self.m_win.mainloop()
1766 
1767  return 0
1768 
1769 
1770 # ------------------------------------------------------------------------------
1771 # main
1772 # ------------------------------------------------------------------------------
1773 if __name__ == '__main__':
1774  app = application();
1775  sys.exit( app.run() );
def createMotorPanel(self, parent, row, col, ctlrkey, motorpos)
Create motor controller motor panel.
def showError(self, msg)
Show error message on status bar.
def setPidParams(self, ck, mk, Kp, Ki, Kd, maxQpps, maxAccel)
def createPidParamsPanel(self, parent, row, col, ctlrkey)
Create motor controller motors PID parameters panel.
Unit test command-line exception class.
def apply(self)
Apply tuning tweaks to controllers callback.
def getLaelapsName(self)
Get Laelaps name.
def __init__(self, msg)
Constructor.
def printUsageErr(self, emsg)
Print usage error.
def cbVelSetpoint(self, ck, mk, fk, v)
Setpoint changed callback.
def createWidgets(self)
Create gui widgets with supporting data and show.
def createVelTuningPanel(self, parent, row, col)
Create velocity tuning panel.
def updateButtonState(self, keys, state)
Update button activation states.
def createMotorsTuningPanel(self, parent, row, col, ctlrpos)
Create motor controller motors tuning panel.
def showEntry(self, w, var, val, fg='black')
Show text on read-only entry.
def cbEnDisVelPlot(self, ck, mk, fk)
Enable/disable velocity plot.
def about(self)
Show about dialog callback.
def getOptions(self, argv=None, kwargs)
Get command-line options.
def createPlotControls(self, parent, row, col)
Create real-time plot controls.
def createStatusBar(self)
Create gui status bar at bottom of gui window.
def createAlarmsPanel(self, parent, row, col, ctlrkey)
Create motor controller alarm panel.
def createHeading(self)
Create top gui heading.
def createCenterPanel(self)
Create robot status and joint state center panel.
def createMenu(self)
Create menu.
def run(self, argv=None, kwargs)
Run application.
Window class supporting application.
def makeWidgets(self, parent, wdescTbl)
Make widgets from widget description table.
msg
error message attribute
def createSetpointsPanel(self, parent, row, col, ctlrkey)
Create motor controller motor velocity setpoints panel.
def createPlotCanvas(self, parent, row, col, width)
Create real-time plot canvas.
def createStatusPanel(self, parent, row, col)
Create motor status panel.
def initRobot(self)
Initialize interface to hek_robot.
def makeWidget(self, parent, widget, wcfg, gcfg)
Make widget.
def initData(self, kw)
Initialize class state data.
def stop(self)
Stop Laelaps.
def updateStatus(self)
Update motor status.
def printUsage(self)
Print Command-Line Usage Message.
def createAlarmWidget(self, parent, row, col, ctlrkey, text, key)
Create alarm widgets and initalize alarm db.
def createLeftButtons(self)
Create gui left hand side buttons.
def destroy(self)
Destroy window callback.
def createButton(self, parent, text, imagefile, command, fg='black')
Create button.
def notimpl(self)
Not implemented callback.
def save(self)
Save tuning to tuning file and controller EEPROM callback.
def showInfo(self, msg)
Show information message on status bar.
def createMotorCtlrPanel(self, parent, row, col, ctlrpos)
Create motor controller panel.
def alignToJustify(self, align)
Map alignment value to justify equivalent.
def finalInits(self)
Final window initializations.
def createPlotPanel(self, parent, row, col, width)
Create real-time plot panel.
def __init__(self, master=None, cnf={}, kw)
Constructor.
def getProductInfo(self)
Get product information.