Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 10:17:18

0001 #!/usr/bin/env python3
0002 #
0003 # GUI for monitoring and controlling a running JANA process
0004 # via ZMQ messages.
0005 #
0006 # This is only useful if the process was started with the janacontrol
0007 # plugin attached.
0008 #
0009 # By default, this uses port 11238 (defined in janacontrol.cc), but
0010 # can use a different port number if specified by the JANA_ZMQ_PORT
0011 # when the JANA process was started.
0012 #
0013 # Copyright 2020, Jefferson Science Associates, LLC.
0014 # Subject to the terms in the LICENSE file found in the top-level directory.
0015 
0016 import os
0017 import sys
0018 import zmq
0019 import json
0020 import threading
0021 import time
0022 from tkinter import *
0023 import tkinter.ttk as ttk
0024 import tkinter.font as tkFont
0025 # from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
0026 # from matplotlib.figure import Figure
0027 # import matplotlib.pyplot as plt
0028 # import numpy as np
0029 
0030 # -- Globals
0031 DONE      = False
0032 PORT      = 11238
0033 HOST      = 'localhost'
0034 SOCKET    = None
0035 
0036 #-----------------------------------------------------------------------------
0037 # MultiColumnListbox
0038 #
0039 # This copied from https://stackoverflow.com/questions/5286093/display-listbox-with-columns-using-tkinter
0040 # I've modified this by Adding the _upsrt method to insert and update data.
0041 # The sortby proc was also made a method of this class.
0042 #
0043 class MultiColumnListbox(object):
0044     def __init__(self, parent, columns, title=None, titlevar=None):
0045         self.parent = parent
0046         self.columns = columns
0047         self.item_map = {} # key=hostname val=item returned by insert
0048         self.tree = None
0049         self._setup_widgets(title, titlevar)
0050         self._build_tree()
0051 
0052     def _setup_widgets(self, title, titlevar):
0053         self.parent.columnconfigure(0, weight=1)
0054         self.parent.rowconfigure(0, weight=1) # Title
0055         self.parent.rowconfigure(1, weight=10) # TreeView, scrollbars
0056 
0057         if title:
0058             msg = ttk.Label(self.parent, wraplength="4i", justify="left", anchor="n", padding=(10, 2, 10, 6), text=title, width=55)
0059         elif titlevar:
0060             msg = ttk.Label(self.parent, wraplength="4i", justify="left", anchor="n", padding=(10, 2, 10, 6), textvariable=titlevar, width=55)
0061         if 'msg' in locals() : msg.grid(row=0, column=0, sticky=W+E)
0062 
0063         # Container to hold treeview and scrollbars
0064         container = ttk.Frame(self.parent)
0065         container.grid(row=1, column=0, sticky=N+S+W+E)
0066         container.columnconfigure(0, weight=1)
0067         container.rowconfigure(0, weight=1)
0068 
0069         # create a treeview with dual scrollbars
0070         self.tree = ttk.Treeview(container,columns=self.columns, height=15, show="headings")
0071         vsb = ttk.Scrollbar(container,orient="vertical", command=self.tree.yview)
0072         hsb = ttk.Scrollbar(container,orient="horizontal", command=self.tree.xview)
0073         self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
0074         self.tree.grid(column=0, row=0, sticky='nsew', in_=container)
0075         vsb.grid(column=1, row=0, sticky='ns', in_=container)
0076         hsb.grid(column=0, row=1, sticky='ew', in_=container)
0077 
0078     def _build_tree(self):
0079         for col in self.columns:
0080             self.tree.heading(col, text=col.title(), command=lambda c=col: self.sortby(self.tree, c, 0))
0081             # adjust the column's width to the header string
0082             myanchor = E
0083             if col == self.columns[0]: myanchor=W
0084             colwidth = int(tkFont.Font().measure(col.title()))
0085             self.tree.column(col, width=colwidth, anchor=myanchor, stretch=NO)
0086 
0087     # Upsert row in table. "row" should be dictionary with keys for every
0088     # column. If a row for the "host" already exists, its values are
0089     # updated. Otherwise a new row is inserted for that host.
0090     def _upsrt_row(self, row):
0091         # Create a list of the values for each entry corresponding to the column names
0092         row_list = []
0093         for col in self.columns:
0094             if col in row:
0095                 row_list.append( row[col] )
0096             else:
0097                 row_list.append( '' )
0098         item = tuple(row_list)
0099 
0100         # Use first column as key
0101         mykey = item[0]
0102         if len(item) > 1 : mykey += ':' + item[1]
0103         if mykey not in self.item_map.keys():
0104             self.item_map[mykey] = self.tree.insert('', 'end', values=item)
0105         # Update row with new values and adjust column's width if necessary to fit each value
0106         for ix, val in enumerate(item):
0107             self.tree.set(self.item_map[mykey], self.columns[ix], val)
0108             col_w = tkFont.Font().measure(val) + 10
0109             if self.tree.column(self.columns[ix],width=None)<col_w:
0110                 self.tree.column(self.columns[ix], width=col_w)
0111 
0112     # sort tree contents when a column header is clicked on
0113     def sortby(self, tree, col, descending):
0114         # grab values to sort
0115         data = [(tree.set(child, col), child) for child in tree.get_children('')]
0116         # if the data to be sorted is numeric change to float
0117         #data =  change_numeric(data)
0118         # now sort the data in place
0119         data.sort(reverse=descending)
0120         for ix, item in enumerate(data): tree.move(item[1], '', ix)
0121         # switch the heading so it will sort in the opposite direction
0122         tree.heading(col, command=lambda column=col: self.sortby(tree, column, int(not descending)))
0123 
0124     def clear(self):
0125         self.tree.delete(*self.tree.get_children()) # delete all items in tree
0126         self.item_map={}
0127 
0128 #-----------------------------------------------------------------------------
0129 # MyWindow   (The Main GUI)
0130 class MyWindow(ttk.Frame):
0131 
0132     #=========================
0133     # __init__
0134     def __init__(self, master=None):
0135         ttk.Frame.__init__(self, master)
0136         self.master = master
0137         self.init_window()
0138         self.cmd_queue = []  # List of commands to send during TimerUpdate call
0139         self.debug_window = None
0140 
0141     #=========================
0142     # init_window
0143     def init_window(self):
0144      
0145         # Find a font family for the banner
0146         preferred_fonts = ['Comic Sans MS', 'URW Gothic', 'Courier']
0147         available_fonts=list(tkFont.families())
0148         self.bannerFont = None
0149         for f in preferred_fonts:
0150             for af in available_fonts:
0151                 if af.startswith( f ):
0152                     self.bannerFont = tkFont.Font(family=af, size=36)
0153                     break
0154             if self.bannerFont : break
0155         if self.bannerFont == None : self.bannerFont = tkFont.Font(family=preferred_fonts[-1], size=36)
0156             
0157         self.labelsFont = tkFont.Font(family="Helvetica", size=16)
0158         self.labmap = {}
0159 
0160         self.master.title('JANA Status/Control GUI')
0161         self.grid(row=0, column=0, sticky=N+S+E+W ) # Fill root window
0162         Grid.rowconfigure(self, 0, weight=1)
0163         Grid.columnconfigure(self, 0, weight=1)
0164 
0165         #----- Banner
0166         bannerframe = ttk.Frame(self)
0167         bannerframe.grid( row=0, sticky=E+W)
0168         ttk.Label(bannerframe, anchor='center', text='JANA Status/Control GUI', font=self.bannerFont, background='white').grid(row=0, column=0, sticky=E+W)
0169         Grid.columnconfigure(bannerframe, 0, weight=1)
0170 
0171         #----- Information Section (Top)
0172         infoframe = ttk.Frame(self)
0173         infoframe.grid( row=1, sticky=N+S+E+W)
0174 
0175         # Process Info
0176         labels = { # keys are names in JSON record, vales are text labels for GUI
0177             'program':'Program Name',
0178             'host'   :'host',
0179             'PID'    :'PID',
0180             'NEventsProcessed': 'Number of Events',
0181             'rate_avg': 'Avg. Rate (Hz)',
0182             'rate_instantaneous': 'Rate (Hz)',
0183             'NThreads': 'Number of Threads',
0184             'cpu_total': 'CPU Total Usage (%)',
0185             'cpu_idle': 'CPU idle (%)',
0186             'cpu_nice': 'CPU nice (%)',
0187             'cpu_sys': 'CPU system (%)',
0188             'cpu_user': 'CPU user (%)',
0189             'ram_tot_GB': 'RAM total (GB)',
0190             'ram_avail_GB': 'RAM avail. (GB)',
0191             'ram_free_GB': 'RAM free (GB)',
0192             'ram_used_this_proc_GB': 'RAM used this proc (GB)'
0193         }
0194         procinfoframe = ttk.Frame(infoframe)
0195         procinfoframe.grid( row=0, column=0, sticky=N+S+E+W, padx=30, ipadx=10 )
0196         for i,(varname,label) in enumerate(labels.items()):
0197             self.labmap[varname] = StringVar()
0198             ttk.Label(procinfoframe, anchor='e', text=label+': ', font=self.labelsFont).grid(row=i, column=0, sticky=E+W)
0199             ttk.Label(procinfoframe, anchor='w', textvariable=self.labmap[varname], font=self.labelsFont  ).grid(row=i, column=1, sticky=E+W)
0200 
0201         # Initialize all labels
0202         for key in self.labmap.keys(): self.labmap[key].set('---')
0203 
0204         # Factory Info (right)
0205         facinfoframe = ttk.Frame(infoframe)
0206         facinfoframe.grid( row=0, column=1, sticky=N+S+E+W, padx=30, ipadx=10 )
0207         self.facinfo_columns = ['FactoryName', 'FactoryTag', 'DataType', 'plugin']
0208         self.factory_info = MultiColumnListbox(facinfoframe, columns=self.facinfo_columns, title='Factories')
0209 
0210         #----- Control Section (Middle)
0211         controlframe = ttk.Frame(self)
0212         controlframe.grid( row=2, sticky=N+S+E+W)
0213         controlframe.columnconfigure(0, weight=1)
0214 
0215         # Nthreads
0216         nthreadsframe = ttk.Frame(controlframe)
0217         nthreadsframe.grid( row=0, sticky=E+W)
0218         ttk.Label(nthreadsframe, anchor='center', text='Num. threads', background='white').grid(row=0, column=0, columnspan=2, sticky=E+W)
0219         # Grid.columnconfigure(nthreadsframe, 0, weight=1)
0220 
0221         but1 = ttk.Button(nthreadsframe, text='--', command=self.DecrNThreads)
0222         but1.grid(row=1, column=0)
0223         but2 = ttk.Button(nthreadsframe, text='++', command=self.IncrNthreads)
0224         but2.grid(row=1, column=1)
0225         but3 = ttk.Button(controlframe, text='Debugger', command=self.OpenDebugger)
0226         but3.grid(row=0, column=2)
0227         # but4 = ttk.Button(controlframe, text='test', command=self.Test)
0228         # but4.grid(row=1, column=0, sticky=W)
0229 
0230         remotequitButton = ttk.Button(controlframe, text="Quit Remote",command=self.QuitRemote)
0231         remotequitButton.grid( row=2, column=0, columnspan=10, sticky=E+W )
0232 
0233         #----- GUI controls (Bottom)
0234         guicontrolframe = ttk.Frame(self)
0235         guicontrolframe.grid( row=3, sticky=E+W )
0236         guicontrolframe.columnconfigure(1, weight=1)
0237 
0238         closeButton = ttk.Button(guicontrolframe, text="Quit",command=self.Quit)
0239         closeButton.grid( row=0, column=2, sticky=E+W)
0240         closeButton.columnconfigure(0, weight=1)
0241 
0242         #===== Configure weights of layout
0243         Grid.rowconfigure(self, 0, weight=1)  # Info
0244         Grid.rowconfigure(self, 1, weight=10)  # Info
0245         Grid.rowconfigure(self, 2, weight=10)  # Control
0246         Grid.rowconfigure(self, 3, weight=1)   # GUI controls
0247         Grid.columnconfigure(self, 0, weight=1)
0248 
0249     #=========================
0250     # PrintResult
0251     #
0252     # This is used as the callback for simple commands that don't need to process
0253     # the return values, but would like a success or failure message to be
0254     # printed.
0255     def PrintResult(self, cmd, result):
0256         print('command: ' + cmd + '   -- result: ' + result)
0257 
0258     #=========================
0259     # Quit
0260     def Quit(self):
0261         # This is trickier than you might first guess. If the debug window has been
0262         # opened, then the remote process may be in "debug mode" so event processing
0263         # is blocked. In this case, we want to release it before we disconnect by
0264         # sending it a message to go out of debug mode. Messages are sent by placing
0265         # them on the command_queue and letting TimerThread handle it. If we are not
0266         # actually connected to a remote process, then the ThreadTimer routine is
0267         # likely blocking on the SOCKET.recv() and will only continue if we close the
0268         # socket. This boils down to wanting to either send a message on the socket
0269         # if it is connected or close the socket if it is not. Unfortunately, zmq
0270         # does not provide a way to check this (it kind of goes against the some core
0271         # design of the REQ-REP model). Thus, what we do to handle all cases is
0272         # queue up a message to be sent and arrange for the Cleanup method to be called
0273         # after a short time in hopes that the message makes it out in time
0274         print('Quitting ...')
0275         if self.debug_window:
0276             self.cmd_queue.append( ('debug_mode 0', self.PrintResult) )
0277             self.after(2000, self.Cleanup)
0278         else:
0279             self.Cleanup()
0280         # time.sleep(0.75) # give TimerThread time to do one iteration so command to exit debug mode is sent to remote process
0281         # Tell TimerThread method to exit at next iteration.
0282         # Note that we defer stopping the main loop until the end of ThreadTimer
0283         # and defer actually destroying the window until the main loop is stopped.
0284         # DONE=True
0285         # print('Quitting ...')
0286 
0287     #=========================
0288     # Cleanup
0289     def Cleanup(self):
0290         global DONE, SOCKET
0291         # This is called to force the TimerThread routine to quit its loop and exit cleanly.
0292         print('  - closing ZMQ socket ...')
0293         DONE=True
0294         if SOCKET:
0295             SOCKET.close(linger=0)    # This will un-block the recv call if it is stuck there
0296             context.destroy(linger=0)
0297             SOCKET = None
0298 
0299     #=========================
0300     # QuitRemote
0301     def QuitRemote(self):
0302         self.cmd_queue.append( ('quit', self.PrintResult) )
0303 
0304     #=========================
0305     # DecrNThreads
0306     def DecrNThreads(self):
0307         self.cmd_queue.append( ('decrement_nthreads', self.PrintResult) )
0308 
0309     #=========================
0310     # IncrNthreads
0311     def IncrNthreads(self):
0312         self.cmd_queue.append( ('increment_nthreads', self.PrintResult) )
0313 
0314     #=========================
0315     # OpenDebugger
0316     def OpenDebugger(self):
0317         if not self.debug_window:
0318             self.debug_window = MyDebugWindow(self)
0319         self.debug_window.RaiseToTop()
0320         self.cmd_queue.append( ('debug_mode 1', self.PrintResult) ) # Go into debug mode
0321 
0322     #=========================
0323     # Test
0324     def Test(self):
0325         self.cmd_queue.append( ('debug_mode 1', self.PrintResult) )
0326         self.cmd_queue.append( ('next_event', self.PrintResult) )
0327         self.cmd_queue.append( ('get_object_count', self.PrintResult) )
0328 
0329         if not self.debug_window:
0330             self.debug_window = MyDebugWindow(self)
0331         self.debug_window.RaiseToTop()
0332 
0333 #=========================
0334     # SetProcInfo
0335     def SetProcInfo(self, cmd, result):
0336         # Remember most recently received proc info
0337         info = json.loads(result)
0338         self.last_info = info
0339 
0340         # Loop over top-level keys in the JSON record and any that
0341         # have a corresponding entry in the labmap member of the
0342         # class, set the value of the label.
0343         labkeys = self.labmap.keys()
0344         for k,v in info.items():
0345             if k in labkeys: self.labmap[k].set(v)
0346 
0347     #=========================
0348     # SetFactoryList
0349     def SetFactoryList(self, cmd, result):
0350         s = json.loads( result )
0351         if 'factories' in s :
0352             for finfo in s['factories']:
0353                 # Make additional entries into the dictionary that use the column names so we can upsrt the listbox
0354                 finfo['FactoryName'] = finfo['factory_name']
0355                 finfo['FactoryTag'] = finfo['factory_tag']
0356                 finfo['plugin'] = finfo['plugin_name']
0357                 finfo['DataType'] = finfo['object_name']
0358                 self.factory_info._upsrt_row(finfo)
0359 
0360     #=========================
0361     # TimerUpdate
0362     #
0363     # This method is run in a separate thread and continuously loops to
0364     # update things outside of the mainloop.
0365     def TimerUpdate(self):
0366         icntr = 0  # keeps track of iteration modulo 1000 so tasks can be performed every Nth iteration
0367         while not DONE:
0368 
0369             # Add some periodic tasks
0370             self.cmd_queue.append( ('get_status', self.SetProcInfo) )
0371             if icntr%4 == 0 : self.cmd_queue.append( ('get_factory_list', self.SetFactoryList) )
0372             if self.debug_window :
0373                 if icntr%2 == 0 : self.cmd_queue.append( ('get_object_count', self.debug_window.SetFactoryObjectCountList) )
0374 
0375             # Send any queued commands
0376             while len(self.cmd_queue)>0:
0377                 try:
0378                     (cmd,callback) = self.cmd_queue.pop(0)
0379                     try:
0380                         SOCKET.send( cmd.encode() )
0381                         message = SOCKET.recv()
0382                         callback(cmd, message.decode())
0383                     except:
0384                         pass
0385                 except:
0386                     # self.cmd_queue.pop(0) raised exception, possibly due to having no more items
0387                     break  # break while len(self.cmd_queue)>0 loop
0388 
0389             # Update icntr
0390             icntr += 1
0391             icntr = (icntr+1)%1000
0392 
0393             # Limit rate through loop
0394             if DONE: break
0395             time.sleep(0.5)
0396 
0397         # Done with timer thread which must mean we are exiting. Tell mainloop() to exit.
0398         root.quit()
0399 
0400 #-----------------------------------------------------------------------------
0401 # MyDebugWindow   (The (optional) Debug GUI)
0402 class MyDebugWindow(ttk.Frame):
0403 
0404     #=========================
0405     # __init__
0406     def __init__(self, parent):
0407         master = Toplevel(parent.master)
0408         ttk.Frame.__init__(self, master)
0409         self.master = master
0410         self.parent = parent
0411         self.master.title("JANA Debugger")
0412 
0413         # Center debug window on main GUI
0414         width = 1200
0415         height = 700
0416         xoffset = self.parent.master.winfo_x() + self.parent.master.winfo_width()/2 - width/2
0417         yoffset = self.parent.master.winfo_y() + self.parent.master.winfo_height()/2 - height/2
0418         geometry = '%dx%d+%d+%d' % (width, height, xoffset, yoffset)
0419         self.master.geometry(geometry)
0420         self.labmap = {}
0421         self.init_window()
0422 
0423         self.Stop() # switch to debug mode when window is first created
0424 
0425     #=========================
0426     # init_window
0427     def init_window(self):
0428 
0429         self.master.title('JANA Debugger')
0430         self.master.rowconfigure(0, weight=1)
0431         self.master.columnconfigure(0, weight=1)
0432         self.grid(row=0, column=0, sticky=N+S+E+W , ipadx=10, ipady=10, padx=10, pady=10) # Fill root window
0433         self.rowconfigure(0, weight=1 )  # bannerframe
0434         self.rowconfigure(1, weight=1)   # eventinfoframe
0435         self.rowconfigure(2, weight=50)  # infoframe
0436         self.rowconfigure(3, weight=1)   # controlframe
0437         self.rowconfigure(10, weight=1 )  # guicontrolframe
0438         self.columnconfigure(0, weight=1)
0439 
0440         self.labelsFont = self.parent.labelsFont
0441         self.bannerFont = self.parent.bannerFont
0442 
0443         #----- Banner
0444         bannerframe = ttk.Frame(self)
0445         bannerframe.grid( row=0, column=0, sticky=E+W)
0446         bannerframe.columnconfigure( 0, weight=1)
0447         ttk.Label(bannerframe, anchor='center', text='JANA Debugger', font=self.bannerFont, background='white').grid(row=0, column=0, sticky=E+W)
0448 
0449         #----- Event Info
0450         eventinfoframe = ttk.Frame(self)
0451         eventinfoframe.grid( row=1, column=0, sticky=N+S+E+W)
0452 
0453         # Process Info
0454         labels = { # keys are names in JSON record, vales are text labels for GUI
0455             'program':'Program Name',
0456             'host'   :'host',
0457             'PID'    :'PID',
0458             'run_number': 'Run Number',
0459             'event_number': 'Event Number',
0460         }
0461         procinfoframe = ttk.Frame(eventinfoframe)
0462         procinfoframe.grid( row=0, column=0, sticky=N+S+E+W, padx=30 )
0463         for i,(varname,label) in enumerate(labels.items()):
0464             self.labmap[varname] = StringVar()
0465             ttk.Label(procinfoframe, anchor='e', text=label+': ', font=self.labelsFont).grid(row=i, column=0, sticky=E+W)
0466             ttk.Label(procinfoframe, anchor='w', textvariable=self.labmap[varname]  ).grid(row=i, column=1, sticky=E+W)
0467 
0468         # Initialize all labels
0469         for key in labels.keys(): self.labmap[key].set('---')
0470 
0471         #----- Factory Info
0472         infoframe = ttk.Frame(self)
0473         infoframe.grid(row=2, column=0, sticky=N+S+E+W, ipadx=5)
0474         infoframe.rowconfigure(0, weight=1)
0475         infoframe.columnconfigure(0, weight=1)  # facinfoframe
0476         infoframe.columnconfigure(1, weight=1)   # objinfoframe
0477         infoframe.columnconfigure(2, weight=1)  # objfieldsframe
0478 
0479         facinfoframe = ttk.Frame(infoframe, style='FrameYellow.TFrame')
0480         facinfoframe.grid( row=0, column=0, sticky=N+S+E+W)
0481         self.facinfo_columns = ['FactoryName', 'FactoryTag', 'DataType', 'plugin', 'Num. Objects']
0482         self.factory_info = MultiColumnListbox(facinfoframe, columns=self.facinfo_columns, title='Factories')
0483         self.factory_info.tree.bind("<<TreeviewSelect>>", self.OnSelectFactory)
0484 
0485         # Object Info
0486         objinfoframe = ttk.Frame(infoframe)
0487         objinfoframe.grid( row=0, column=1, sticky=N+S+E+W)
0488         self.labmap['object_type'] = StringVar()
0489         self.objinfo_columns = ['address']
0490         self.object_info = MultiColumnListbox(objinfoframe, columns=self.objinfo_columns, titlevar=self.labmap['object_type'])
0491         self.object_info.tree.bind("<<TreeviewSelect>>", self.OnSelectObject)
0492 
0493         # Object fields
0494         objfieldsframe = ttk.Frame(infoframe)
0495         objfieldsframe.grid( row=0, column=2, sticky=N+S+E+W)
0496         self.labmap['field_object_addr'] = StringVar()
0497         self.objfields_columns = ['name','value', 'type']
0498         self.object_fields = MultiColumnListbox(objfieldsframe, columns=self.objfields_columns, titlevar=self.labmap['field_object_addr'])
0499         self.object_fields.tree.column(0, anchor=E)  # Make name column right justified
0500         self.object_fields.tree.column(1, anchor=E)  # Make value column right justified
0501 
0502         #----- Control Section (Middle)
0503         controlframe = ttk.Frame(self)
0504         controlframe.grid( row=3, column=0, sticky=N+S+E+W)
0505         controlframe.columnconfigure(0, weight=1)
0506 
0507         # Run/Stop
0508         runstopframe = ttk.Frame(controlframe)
0509         runstopframe.grid( row=0, column=0, sticky=E+W)
0510         ttk.Label(runstopframe, anchor='center', text='Run/Stop', background='white').grid(row=0, column=0, columnspan=2, sticky=E+W)
0511         self.labmap['runstop'] = StringVar()
0512         self.runstoplab = ttk.Label(runstopframe, anchor='center', textvariable=self.labmap['runstop'], background='#FF0000')
0513         self.runstoplab.grid(row=1, column=0, columnspan=2, sticky=E+W)
0514         ttk.Button(runstopframe, text='Stop', command=self.Stop).grid(row=2, column=0)
0515         ttk.Button(runstopframe, text='Run',  command=self.Run ).grid(row=2, column=1)
0516         ttk.Button(runstopframe, text='Next Event',  command=self.NextEvent ).grid(row=3, column=0, columnspan=2)
0517 
0518         #----- GUI controls (Bottom)
0519         guicontrolframe = ttk.Frame(self)
0520         guicontrolframe.grid( row=10, column=0, sticky=E+W )
0521         guicontrolframe.columnconfigure(0, weight=1)
0522 
0523         closeButton = ttk.Button(guicontrolframe, text="Close",command=self.HideWindow)
0524         closeButton.grid( row=0, column=0, sticky=E)
0525         closeButton.columnconfigure(0, weight=1)
0526 
0527     #=========================
0528     # HideWindow
0529     def HideWindow(self):
0530         self.parent.cmd_queue.append( ('debug_mode 0', self.parent.PrintResult) )
0531         self.master.withdraw()
0532 
0533     #=========================
0534     # RaiseToTop
0535     def RaiseToTop(self):
0536         self.master.deiconify()
0537         self.master.focus_force()
0538         self.master.attributes('-topmost', True)
0539         self.after_idle(self.master.attributes,'-topmost',False)
0540 
0541     #=========================
0542     # Run
0543     def Run(self):
0544         self.parent.cmd_queue.append( ('debug_mode 0', self.parent.PrintResult) )
0545         self.running = True
0546         self.labmap['runstop'].set('running')
0547         self.runstoplab.config(background='#00FF00')
0548 
0549     #=========================
0550     # Stop
0551     def Stop(self):
0552         self.parent.cmd_queue.append( ('debug_mode 1', self.parent.PrintResult) )
0553         self.running = False
0554         self.labmap['runstop'].set('stopped')
0555         self.runstoplab.config(background='#FF0000')
0556 
0557     #=========================
0558     # NextEvent
0559     def NextEvent(self):
0560         self.parent.cmd_queue.append( ('next_event', self.parent.PrintResult) )
0561 
0562     #=========================
0563     # OnSelectFactory
0564     def OnSelectFactory(self, event):
0565         for item in self.factory_info.tree.selection():
0566             row = self.factory_info.tree.item(item)
0567             if row :
0568                 vals = row['values']
0569                 self.labmap['object_type'].set(vals[2])
0570                 cmd = 'get_objects %s %s %s' % (vals[2], vals[0], vals[1]) #  object_name factory_name factory_tag
0571                 print('cmd='+cmd)
0572                 self.parent.cmd_queue.append( (cmd, self.SetFactoryObjectList) )
0573                 self.object_fields.clear() # clear object fields 
0574 
0575     #=========================
0576     # OnSelectObject
0577     def OnSelectObject(self, event):
0578         for item in self.object_info.tree.selection():
0579             row = self.object_info.tree.item(item)
0580             if row:
0581                 addr = row['values'][0]
0582                 label = addr + ' (' + self.labmap['object_type'].get() + ')'
0583                 self.labmap['field_object_addr'].set(label)
0584                 s = self.last_object_list
0585                 if addr in s['objects'].keys():
0586                     fields = s['objects'][addr]
0587                     self.object_fields.clear()
0588                     for row in fields:
0589                         self.object_fields._upsrt_row(row)
0590 
0591     #=========================
0592     # SetFactoryObjectCountList
0593     #
0594     # This is called when data is received that contains a list of factories and their
0595     # object counts for a specific event. Other metadata like run, event, procid, and
0596     # host (if it is included) is used to update the labels at the top of the debugger
0597     # window.
0598     #
0599     # This also checks if any of run,event, or procid has changed and if so, calls
0600     # UpdateNewEvent() to update other TreeViews.
0601     def SetFactoryObjectCountList(self, cmd, result):
0602         s = json.loads( result )
0603 
0604         # Set host/proc/event info/ (assume any JSON keys that match keys in labmap should be set)
0605         labkeys = self.labmap.keys()
0606         run_changed = False
0607         event_changed = False
0608         procid_changed = False
0609         for k,v in s.items():
0610             if k == 'run_number': run_changed = (v != self.labmap[k].get())
0611             if k == 'event_number': event_changed = (v != self.labmap[k].get())
0612             if k == 'procid': procid_changed = (v != self.labmap[k].get())
0613             if k in labkeys: self.labmap[k].set(v)
0614 
0615         if 'factories' in s :
0616             for finfo in s['factories']:
0617                 # Make additional entries into the dictionary that use the column names so we can upsrt the listbox
0618                 finfo['FactoryName'] = finfo['factory_name']
0619                 finfo['FactoryTag'] = finfo['factory_tag']
0620                 finfo['plugin'] = finfo['plugin_name']
0621                 finfo['DataType'] = finfo['object_name']
0622                 finfo['Num. Objects'] = finfo['nobjects']
0623                 self.factory_info._upsrt_row(finfo)
0624 
0625         # Call UpdateNewEvent if any of run,event, or procid changed
0626         if run_changed or event_changed or procid_changed: self.UpdateNewEvent()
0627 
0628     #=========================
0629     # UpdateNewEvent
0630     #
0631     # This is called when a new run/event/procid combination is received and identified
0632     # in SetFactoryObjectCountList(). Its purpose is to update the various TreeViews
0633     def UpdateNewEvent(self):
0634         # Clear the object field TreeView
0635         self.object_fields.clear()
0636         self.last_object_list = None
0637 
0638         # Only clear object addresses if there is no factory selected. If one is selected, then
0639         # we want to preserve any selected object address so the fields can be updated later when
0640         # SetFactoryObjectList() gets called after a new set of objects is received. A request
0641         # for new objects is automatically issued when self.factory_info.tree.selection_set(item)
0642         # triggers OnSelectFactory() above.
0643         self.object_info.clear()
0644         # if len(self.factory_info.tree.selection()) == 0:
0645         #   self.object_info.clear()
0646 
0647         # For any currently selected factory, re-select it so the OnSelectFactory() gets called
0648         # the objects list gets updated
0649         for item in self.factory_info.tree.selection():
0650             self.factory_info.tree.selection_set(item)
0651 
0652     #=========================
0653     # SetFactoryObjectList
0654     #
0655     # This fills the TreeView of object addresses. If an address is currently selected that
0656     # is not in the list of addresses we are inserting, then the object_fields TreeView
0657     # is cleared. If it is, then the object is re-selected to trigger re-displaying the
0658     # object fields. This is needed in the case that an address is recycled and the contents
0659     # have actually changed.
0660     def SetFactoryObjectList(self, cmd, result):
0661         # Check if there is a currently selected address and remember it if there is
0662         addr = None
0663         items = self.object_info.tree.selection()
0664         if items:
0665             addr = self.object_info.tree.item(items[0])['values'][0] # copy value of first selected item
0666 
0667         try:
0668             s = json.loads( result )
0669         except json.decoder.JSONDecodeError as e:
0670             print('ERROR decoding JSON: ' + e.msg)
0671         self.last_object_list = s
0672         self.object_info.clear() # delete all items in tree
0673         for a in s['objects'].keys():
0674             row = {'address':a}
0675             self.object_info._upsrt_row(row)
0676             if a == addr:
0677                 # Address was already selected. Re-select it now
0678                 item_to_select = self.object_info.tree.get_children()[-1]
0679         if item_to_select: self.object_info.tree.selection_set(item_to_select)
0680 
0681 #=========================
0682 # Usage
0683 def Usage():
0684     mess='''
0685     Usage:
0686             jana-control.py [--help] [--host HOST] [--port PORT]
0687     
0688 This script will open a GUI window that will monitor a running JANA process.
0689 The process can be running on either the local node or a remote node. For
0690 this to work, the following criteria must be met:
0691 
0692 1. JANA must have been compiled with ZEROMQ support. (This relies
0693    on cmake find_package(ZEROMQ) successfully finding it when camke
0694    is run.)
0695 
0696 2. The python3 environment must me present and have zmq installed.
0697    (e.g. pip3 install zmq)
0698 
0699 3. The JANA process must have been started with the janacontrol plugin.
0700    This should generally be added to the *end* of the plugin list
0701    like this:
0702    
0703       -Pplugins=myplugin1,myplugin2,janacontrol
0704 
0705 By default, it will try to attach to port 11238 on the localhost. It
0706 does not matter whether the JANA process is already running or not.
0707 It will automatically connect when it does and reconnect if the process
0708 is restarted.
0709 
0710 The following command line options are available:
0711 
0712 -h, --help     Print this Usage statement and exit
0713 --host HOST    Set the host of the JANA process to monitor
0714 --port PORT    Set the port on the host to connect to
0715 
0716 n.b. To change the port used by the remote JANA process set the
0717 jana:zmq_port configuration parameter.
0718 
0719 Debugger
0720 --------------
0721 The GUI can be used to step through events in the JANA process and
0722 view the objects, with some limitations. If the data object inherits
0723 from JObject then it will display fields obtained from the subclass'
0724 Summarize method. (See JObject::Summarize for details). If the data
0725 object inherits from ROOT's TObject then an attempt is made to extract
0726 the data members via the dictionary. Note that this relies on the
0727 dictionary being available in the plugin and there are limitations
0728 to the complexity of the objects that can be displayed.
0729 
0730 When the debugger window is opened (by pushing the "Debugger" button
0731 on the main GUI window), it will stall event processing so that single
0732 events can be examined and stepped through. To stall processing on the
0733 very first event, the JANA process should have the jana:debug_mode
0734 config. parameter set to a non-zero value when it is started. e.g.
0735 
0736 jana -Pplugins=myplugin1,myplugin2,janacontrol -Pjana:debug_mode=1 file1.dat
0737 
0738 Once an event is loaded, click on a factory to display a list of 
0739 objects it created for this event (displayed as the object's hexadecimal
0740 address). Click on an object to display its content summary (if they
0741 are accessible). n.b. clicking on a factory will NOT cause the factory
0742 algorithm to activate and create objects for the event. It will only
0743 display objects created by other plugins having activated the algorithm.
0744     '''
0745     return mess
0746 
0747 
0748 #=============================================================================
0749 #------------------- main  (rest of script is in global scope) ---------------
0750 
0751 
0752 # Parse the command line parameters
0753 if any(item in ['-h', '-help', '--help'] for item in sys.argv):
0754     print( Usage() )
0755     sys.exit()
0756 Nargs = len(sys.argv)
0757 for i,arg in enumerate(sys.argv):
0758     if i==0 : continue # skip script name
0759     if (arg=='--host') and (i+1<Nargs): HOST = sys.argv[i+1]
0760     if (arg=='--port') and (i+1<Nargs): PORT = sys.argv[i+1]
0761 
0762 print('\nAttempting connection on host="'+HOST+'" port='+str(PORT))
0763 print('For help, run "jana-control.py --help"\n')
0764 
0765 os.environ['TK_SILENCE_DEPRECATION'] = '1'  # Supress warning on Mac OS X about tkinter going away
0766 
0767 # Create window
0768 root = Tk()
0769 style = ttk.Style()
0770 style.theme_use('classic')
0771 style.configure('FrameRed.TFrame',   background='red'  )  #useful for debugging
0772 style.configure('FrameGreen.TFrame', background='green')  #useful for debugging
0773 style.configure('FrameBlue.TFrame',  background='blue' )  #useful for debugging
0774 style.configure('FrameCyan.TFrame',  background='cyan' )  #useful for debugging
0775 style.configure('FrameYellow.TFrame',  background='yellow' )  #useful for debugging
0776 style.configure('FrameViolet.TFrame',  background='violet' )  #useful for debugging
0777 root.geometry("900x700+2500+0")
0778 Grid.rowconfigure(root, 0, weight=1)
0779 Grid.columnconfigure(root, 0, weight=1)
0780 app = MyWindow(root)
0781 
0782 #  Only single 0MQ context is needed
0783 context = zmq.Context()
0784 connstr = "tcp://" + HOST + ":" + str(PORT)
0785 print('Connecting to ' + connstr )
0786 SOCKET = context.socket( zmq.REQ )
0787 SOCKET.connect(connstr)
0788 
0789 # Create a thread for periodic updates of main GUI window
0790 threads = []
0791 t = threading.Thread(target=app.TimerUpdate)
0792 t.start()
0793 threads.append(t)
0794 
0795 # Run main GUI loop until user closes window
0796 root.mainloop()
0797 root.destroy()
0798 
0799 #-----------------------------------------------------------------------------
0800 print('GUI finished. Cleaning up ...')
0801 
0802 print('  - joining threads ...')
0803 for t in threads: t.join()
0804 
0805 print('\nFinished\n')