Green Monster Doc
Green Monster appearance as it appeared on 2019-04-10.
Quick Links
- PyGreenMonster Github Repo
- Code repository
- TKinter Tutorial 1
- Shortest of the tutorials covering the GUI library TKinter. Includes videos if you find that more helpful.
- TKinter Tutorial 2
- A more in-depth tutorial of TKinter, covers far more than is necessary to accomplish Green Monster tasks.
- A Partial API for TKinter
- This acts as a tutorial but also the links to the TKinter classes give a list of their built-in functions and parameters, and can be a handy reference after you get the hang of the GUI programming style.
- An API of the ctypes library
- The only one I anticipate using is cdll, which pulls in functions from shared libraries.
Functionality Outline
The purpose of reworking the Green Monster is so that the GUI will not rely on the ROOT GUI libraries which are old, and difficult to extend. Also newer ROOT versions may end up deprecating or changing the functionality that the 17 year-old code relies on. This presents a challenge in that the new GUI has to interface with the legacy code in cfSockCli.c and cfSockSer.c.
The Green Monster now uses the python language GUI library TKinter, which is inbuilt into all distributions of python 3. The main file for the GUI is on adaq at /adaqfs/home/ajzec/PyGreenMonster/GreenMonster.py
The python files that create the GUI are in the same directory as that file, while the C++ files providing the Green Monster's back-end are located at /adaqfs/home/ajzec/PyGreenMonster/cfSock/
Each of the tabs in the GUI have their own python class file which acts as an interface to their own C++ class file. That C++ class file then becomes an interface to cfSockCli.c
which is the legacy code that eventually attaches to the VXWorks boards in the counting house and injector crates.
The function of the Green Monster is to pass options, and variables up and down these levels.
Layer Concept
In order to keep the new Green Monster as extensible as possible, there needs to be several layers to separate the python classes from the C classes, and the server-side code. There are six in total, and the philosophy of the Green Monster design is to have as few layers passing data between each other as possible.
Layer 1: Green Monster GUI
This is the layer that contains the Green Monster class and only occupies GreenMonster.py
. Here the various tabs at the top of the GM menu are created, though they aren't filled in this layer. The ability to create and quit the window is also here.
Layer 2: Tab Classes
This is the layer where most GUI development will happen, as well as the home of most of the translation of the original GUI code. The GUI is divided up into several tabs each with their own functionality. Beam Modulation is its own tab, as well as setting scan/clean data, etc. There is an "Expert" tab under which other tabs live, including tabs controlling the HAPPEX timing boards, and VQWK ADCs. Generally, any option that is intended to be changed during a run, goes into the expert tab section.
Each tabs its own python class, kept in the PyGreenMonster/tabs
folder. A minimal class has a constructor, class variables for the input fields, and methods laying out the functions of the tabs. If you intend to add entry fields, radio buttons, dropdown menus, or any other widget, they should go in here.
A tutorial on how to create a new tab class is in this page below.
Layer 3: utils.py
This layer is a single file in PyGreenMonster/
called utils.py
which serves two purposes:
- As a place for resources that all tab files need to access (such as, command type codes, crate IDs and global functions)
- As a place for the function
send_command()
which sends a command tocfSockCli.c
This layer should require minimal editing because the test with passing information to cfSock has already been done.
Layer 4: cfSockCli.c
This file is in PyGreenMonster/cfSock/cfSockCli.c
and contains the function GMSockCommand()
which reads data from utils.py, puts it into a C struct and sends it to a socket server running on one of our five crates. The five crates that cfSockCli has programmed are:
- Counting House
- Injector
- Left Spectrometer
- Right Spectrometer
- Test Crate
Layer 5: cfSockSer.c
The code for this file is bundled up in PyGreenMonster/cfSock/cfSockSer.c
however, the client-side of PyGreenMonster does not run it. Therefore any changes to the code won't have any effect on functionality. Unless you know how to reload the server-side file to the VXWorks boards, you shouldn't edit it. Also, compiling it requires the VXWorks compiler. However, it's included in the repository because it does contain print statements to verify that cfSockSer is running on the VXWorks side.
Layer 6: VXWorks Board
The board itself is the final layer, the one that has controls that operate the DAQ. This is the destination all settings in the Green Monster. Each tab is for a single board on one or more DAQ crates. Also, the green monster should correctly initialize by pulling the current values from these boards on startup.
Usage
The Green Monster requires python 3 to run. After navigating to the directory run it with python3 GreenMonster.py
The Green Monster is still in active development, so many functions are not implemented, and others are broken. As of 2019-04-10 the only functioning tab is the "Timeboard" tab in the "Expert" section. This section will update as development continues.
Creating a New Tab
I don't anticipate the Green Monster needing many more tabs (except for maybe spectrometer interfaces) but I'm including this walkthrough to familiarize people with how the python GUI works, as well as to demonstrate the style I used in building it.
Let's say, for the sake of demonstration, I want to add a new tab to the GM. And as the name of the GUI implies, it should probably be about baseball. Let's say I want to add a new tab to display a player's information in the style of Baseball Reference. To create the tab itself, first, I need to edit GreenMonster.py
. In the function create_widgets()
look for the variable tab_titles
which is an array of the names of each tab. (The expert tab has its own functions where everything in there gets defined, but let's ignore this for now.) The block of code I'm looking at looks like this:
def create_widgets(self): gui_style = ttk.Style() gui_style.configure('My.TButton', foreground=u.green_color) gui_style.configure('My.TFrame', background=u.green_color) tab_control = ttk.Notebook(self.win) tab_titles = ['BMW', 'ScanUtil', 'Expert'] for title in tab_titles: tab = ttk.Frame(tab_control, width=800, height=600, style="My.TFrame") tab_control.add(tab, text=title) if 'BMW' in title: bmw.BMW(tab) elif 'Scan' in title: scan_util.ScanUtil(tab) elif 'Expert' in title: self.expert_tab(tab) tab_control.grid(row=0, column=0, columnspan=2)
To add a tab, add its name as a string to tab_titles
, like so:
tab_titles = ['BMW', 'ScanUtil', 'MLB Example', 'Expert']
The GM window will now have that tab, though it will be totally empty:
TODO: IMAGE 1
Hold off on editing GreenMonster.py
for now, and instead create a new file in the PyGreenMonster/tabs/
folder, and call it whatever you like. I'll call my file baseball_reference_tab.py
. First, import the modules you need to build a GUI (tkinter, and ttk if you're feeling fancy), and define a class. The class can be called whatever you want. I'll name mine "MLB_Ref." Make sure it inherits from tk.Frame
or a similar widget so you can treat it as a tk object. Create a constructor, and also create a frame (or in this case a label frame) and place it in the window.
At this point baseball_reference_tab.py
looks like this:
import tkinter as tk from tkinter import ttk import utils as u
class MLB_Ref(tk.Frame): def __init__(self, tab): self.mlb_frame = tk.LabelFrame(tab, text='MLB Reference', background=u.green_color) self.label = tk.Label(self.mlb_frame, text='Example Text', background=u.green_color) self.label.pack(padx=30, pady=30) self.mlb_frame.pack(padx=20, pady=20, anchor='w')
Just a simple class, and a constructor with no dynamic elements. This will work for now.
Now go back to GreenMonster.py
and add import the new class, and its functionality. Add the following statement in the preamble:
import tabs.baseball_reference_tab as mlb
Also, in the loop in the create_widgets()
function that adds the tabs, add an if statement that when it detects the tab name, executes the constructor of MLB_Ref. The constructor takes the tab object as an argument, so be sure to pass it. The functions should look like this when you're done:
def create_widgets(self): #....Style Code.... tab_control = ttk.Notebook(self.win) tab_titles = ['BMW', 'ScanUtil', 'MLB Example', 'Expert'] for title in tab_titles: tab = ttk.Frame(tab_control, width=800, height=600, style="My.TFrame") tab_control.add(tab, text=title) if 'BMW' in title: bmw.BMW(tab) elif 'Scan' in title: scan_util.ScanUtil(tab) elif 'MLB' in title: mlb.MLB_Ref(tab) elif 'Expert' in title: self.expert_tab(tab) tab_control.grid(row=0, column=0, columnspan=2)
Run the GUI and now your tab should be displaying the TKinter objects in the MLB_Ref class:
TODO: IMAGE 2
Now, let's add functionality to MLB_Ref. Let's make the GUI dynamic. First, get rid of the example label, and add four new labels that for our hypothetical baseball player. Let's say all we care about are the player's name, positions, which side they bat from, and which hand they throw with. First, define the labels for these attributes in the constructor.
def __init__(self, tab): self.mlb_frame = tk.LabelFrame(tab, text='MLB Reference', background=u.green_color) self.name_l = tk.Label(self.mlb_frame, text='Name:', background=u.green_color) self.pos_l = tk.Label(self.mlb_frame, text='Position:', background=u.green_color) self.bats_l = tk.Label(self.mlb_frame, text='Bats:', background=u.green_color) self.throws_l = tk.Label(self.mlb_frame, text='Throws:', background=u.green_color) self.mlb_frame.pack(padx=20, pady=20, anchor='w')
Note that the labels have not been placed into a grid yet, so if you were to run the GUI nothing would show up in the "MLB Example" tab.
Next, let's add an entry field for the players' name. In the same function as above add the line:
self.name_e = tk.Entry(self.mlb_frame)
For longer or shorter entry lines, tk.Entry
has a "width" attribute that you can use to specify how many characters wide the entry should be.
Next, neglecting different types of pitchers, there are only nine positions a player could play at. So it would probably make sense to have a dropdown menu with these nine options. In the same function add an array of the options, a tk.StringVar
to store the selection in, and then define an OptionMenu
self.positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P'] self.pos_var = tk.StringVar() self.pos_var.set('C') self.pos_menu = tk.OptionMenu(self.mlb_frame, self.pos_var, *self.positions)
The first argument in defining any tkinter widget should be the parent widget (or frame, or window) that the current widget is inside. In this sense, tkinter design is hierarchical. In our example, the labels, entry fields, and menus defined here are parented by the label frame defined above, which is parented by the tab object, which is parented by the ttk.Notebook
object which is parented by the tkinter window.
The second argument is variable the menu is tied to. whenever the selected option in the menu is changed, so too will this variable. The last argument is the list of options the menu should have.
Now for how a player bats and throws. Define the following variables in the constructor:
self.bats_opts = ['Left', 'Right', 'Switch'] self.throws_opts = ['Left', 'Right'] self.bats_var = tk.StringVar(); self.bats_var.set(self.bats_opts[0]) self.throws_var = tk.StringVar(); self.throws_var.set(self.throws_opts[0])
Which we'll need to create the widgets.
Now it's time to actually start placing our widgets into a grid, and for that I strongly recommend you create a function specifically for that. I'm going to call it frame_layout()
and place all the widgets (which we defined as class variables in the constructor) into their respective frames.
The way I envision this data layout is that each data field gets its own row, and the entry options should be included in each row. For the labels invoke the following commands:
def frame_layout(self): self.name_l.grid(row=0, column=0, padx=5, pady=10, sticky='W') self.pos_l.grid(row=1, column=0, padx=5, pady=10, sticky='W') self.bats_l.grid(row=2, column=0, padx=5, pady=10, sticky='W') self.throws_l.grid(row=3, column=0, padx=5, pady=10, sticky='W')
Add a function call self.frame_layout()
to the constructor at the of the function. Running you GUI now, the frame should look like this:
TODO: IMAGE 3
The options in grid first specify the row and column that each widget should be placed in (counting from zero from the top left). padx and pady specify how much space should be added horizontally (padx) and vertically (pady). The last option is "sticky" which aligns the placement of the widget in the grid cell. It works by sending a string as if you were looking at the alignment as a compass (with North being up). So the strings, 'N', 'E', 'W', 'S', 'NE', 'NW', 'SE', 'SW' are all legal. 'C' is also legal and centers the widget.
Labels are organized on top of each other in different rows in the same column.
Now let's add the widgets to enter info. First add the entry, and menu widgets in frame_layout()
self.name_e.grid(row=0, column=1, padx=5, pady=10, columnspan=3, sticky='W') self.pos_menu.grid(row=1, column=1, padx=5, pady=10, sticky='W')
Where the "columnspan" option tells the widget how many columns in the grid it should occupy on that row.
Now because there are a handful of options for how a player bats or throws, it's smart to use the tk.Radiobutton
widget to define it. And luckily we have already defined those options in the constructor. Add the following loops to frame_layout()
:
for index, opt in enumerate(self.bats_opts): b = tk.Radiobutton(self.mlb_frame, text=opt, variable=self.bats_var, value=opt, background=u.green_color) b.grid(row=2, column=index + 1, padx=5, pady=10, sticky='W') for index, opt in enumerate(self.throws_opts): b = tk.Radiobutton(self.mlb_frame, text=opt, variable=self.throws_var, value=opt, background=u.green_color) b.grid(row=3, column=index + 1, padx=5, pady=10, sticky='W')
Don't bother making the Radiobuttons into class variables, as what's important is the variables bats_var and throw_var which automatically gets updated with the values specified in the value option.
Now your GUI should look like this:
TODO: IMAGE 4
Great! We can now enter information in different ways! However, we can't do anything with the information so this GUI is basically useless. Let's add a button with a function call that makes this gui into something dynamic. Let's say we want to display information on the GUI at the click of a button.
First define a space for the information to go. Let's say we want another label frame that displays the info we put in. Start by changing the line in the constructor that place the "MLB Reference" label frame. change it to:
self.mlb_frame.grid(row=0, column=0, padx=20, pady=20, sticky='W')
Next, add a second frame called "Information." Add this to the end of the constructor:
self.info_frame = tk.LabelFrame(tab, text='Information', background=u.green_color) info_text = 'Name: \nPosition: \nBats: \nThrows:' self.info_l = tk.Label(self.info_frame, text=info_text, justify='left', background=u.green_color) self.info_l.pack(padx=10, pady=10, anchor='w') self.info_frame.grid(row=0, column=1, padx=20, pady=20, sticky='E')
All we've done is add a frame with a label inside it. Running the GUI produces this result:
TODO: IMAGE 5
The label is defined but not dynamic.
Now define a function that would both change the label in the GUI and print information to the console. It's nice to have something to cross-check with. Create the following function and add it to the class:
def print_info(self): info_str = 'Name: ' + self.name_e.get() + '\n' info_str += 'Position: ' + self.pos_var.get() + '\n' info_str += 'Bats: ' + self.bats_var.get() + '\n' info_str += 'Throws: ' + self.throws_var.get() + '\n' self.info_l['text'] = info_str print('Player Info from GUI:') print(' Name: ' + self.name_e.get()) print(' Position: ' + self.pos_var.get()) print(' Bats: ' + self.bats_var.get()) print(' Throws: ' + self.throws_var.get())
The first half of this function create a string for the label, and then takes the info label (a class variable) accesses the attribute 'text' and changes it. The second half of the function is just print statements of the same info.
And finally, add a button at the bottom of the "MLB Reference" frame and link it to the function you just defined. Add the following lines to the end of the frame_layout()
function:
button = tk.Button(self.mlb_frame, text='Print out Info', background=u.green_color, command=self.print_info) button.grid(row=4, column=1, padx=10, pady=15, columnspan=2, sticky='W')
when creating a button the option "command" allows you to input the name of a function (or a lambda) you want to execute whenever the button is pressed. Therefore whenever the button is pressed the function replacing the label text gets called.
Now your GUI should look like this when run:
TODO: IMAGE 6
Enter some information, change up the defaults. Click the button and wouldn't you know it:
TODO: IMAGE 7
We've changed the information in the label frame on the right! And even better, enter some more information and change it again, and it still works:
TODO: IMAGE 8
Looking at the console, we can see the function call has also printed out the info we specified as well:
TODO: IMAGE 9
And there we have it. We've created a new tab, added data fields to it, and gave it some dynamic functionality. Just in case you got lost in this tutorial, this is the file baseball_reference_tab.py
in full:
import tkinter as tk from tkinter import ttk import utils as u class MLB_Ref(tk.Frame): def __init__(self, tab): self.mlb_frame = tk.LabelFrame(tab, text='MLB Reference', background=u.green_color) self.name_l = tk.Label(self.mlb_frame, text='Name:', background=u.green_color) self.pos_l = tk.Label(self.mlb_frame, text='Position:', background=u.green_color) self.bats_l = tk.Label(self.mlb_frame, text='Bats:', background=u.green_color) self.throws_l = tk.Label(self.mlb_frame, text='Throws:', background=u.green_color) self.name_e = tk.Entry(self.mlb_frame) self.positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P'] self.pos_var = tk.StringVar() self.pos_var.set('C') self.pos_menu = tk.OptionMenu(self.mlb_frame, self.pos_var, *self.positions) self.bats_opts = ['Left', 'Right', 'Switch'] self.throws_opts = ['Left', 'Right'] self.bats_var = tk.StringVar(); self.bats_var.set(self.bats_opts[0]) self.throws_var = tk.StringVar(); self.throws_var.set(self.throws_opts[0]) self.frame_layout() self.mlb_frame.grid(row=0, column=0, padx=20, pady=20, sticky='W') self.info_frame = tk.LabelFrame(tab, text='Information', background=u.green_color) info_text = 'Name: \nPosition: \nBats: \nThrows:' self.info_l = tk.Label(self.info_frame, text=info_text, justify='left', background=u.green_color) self.info_l.pack(padx=10, pady=10, anchor='w') self.info_frame.grid(row=0, column=1, padx=20, pady=20, sticky='E') def frame_layout(self): self.name_l.grid(row=0, column=0, padx=5, pady=10, sticky='W') self.pos_l.grid(row=1, column=0, padx=5, pady=10, sticky='W') self.bats_l.grid(row=2, column=0, padx=5, pady=10, sticky='W') self.throws_l.grid(row=3, column=0, padx=5, pady=10, sticky='W') self.name_e.grid(row=0, column=1, padx=5, pady=10, columnspan=3, sticky='W') self.pos_menu.grid(row=1, column=1, padx=5, pady=10, sticky='W') for index, opt in enumerate(self.bats_opts): b = tk.Radiobutton(self.mlb_frame, text=opt, variable=self.bats_var, value=opt, background=u.green_color) b.grid(row=2, column=index + 1, padx=5, pady=10, sticky='W') for index, opt in enumerate(self.throws_opts): b = tk.Radiobutton(self.mlb_frame, text=opt, variable=self.throws_var, value=opt, background=u.green_color) b.grid(row=3, column=index + 1, padx=5, pady=10, sticky='W') button = tk.Button(self.mlb_frame, text='Print out Info', background=u.green_color, command=self.print_info) button.grid(row=4, column=1, padx=10, pady=15, columnspan=2, sticky='W') def print_info(self): info_str = 'Name: ' + self.name_e.get() + '\n' info_str += 'Position: ' + self.pos_var.get() + '\n' info_str += 'Bats: ' + self.bats_var.get() + '\n' info_str += 'Throws: ' + self.throws_var.get() + '\n' self.info_l['text'] = info_str print('Player Info from GUI:') print(' Name: ' + self.name_e.get()) print(' Position: ' + self.pos_var.get()) print(' Bats: ' + self.bats_var.get()) print(' Throws: ' + self.throws_var.get())