DIY Scoreboard

We look at a broadcast video system network that uses Python code to control a video router and check out another program that creates a scoreboard.

Each year my church hosts a basketball league, and several years ago we wanted to upgrade to digital scoreboards because our existing classic board was showing its age. The first version of this new scoreboard software was written in Python and used the GTK toolkit to create the public display. A web page designed for an iPad allowed courtside control.

The gym is a shared space that also hosts a meeting room, party hall, and general-purpose room. To support these roles, two NUC small-form-factor computers drive the displays (100-inch LCD TVs). Windows was necessary to support all of the display software so that PowerPoint and ProPresenter would run natively.

In general, a video system has a hub-and-spoke-style network wherein each device has a dedicated home that runs to a video switcher (Figure 1). The video switcher, as its name implies, accepts all of the video inputs and allows the operator to pick which one should be displayed, overlaid, or otherwise presented to the final output (Figure 2).

F1_video-diagram.tif
Figure 1: Video system signal flow. Displays shown in blue can be seen in the control room photo (Figure 2).
F2_control-room-wide.tif
Figure 2: The video control room is a converted classroom. From left to right, the first two monitors in the background are the main displays of the NUCs running the scoreboard software. The text window is the output of the Python program, and the scoreboard itself is on the secondary monitor output. The third monitor is routable from the video matrix to select what is shown. In the foreground are the matrix control touchscreen and the camera controller. The tall monitor is the switcher multiview. The NUCs and the matrix are visible at the very bottom of the rack under the table.

Displays are slightly more complicated because they need to select the switched video program from the video switcher or the scoreboards at any given time, which is accomplished by a matrix router. See the “Matrix Routers” box for more details.

Matrix Routers

In the video world, a router is a really helpful device. It accepts some number of video signals in and can send them to any of its destinations. This example uses a small four-input by four-output version, but other versions are available with up to thousands of I/O points.

Think of the router as a grid with sources (video signals – see the “Video Signals and Conversions” box) coming in on the left and going out along the bottom. Each column (an output or destination) is allowed one connection. Inputs can connect to as many outputs as needed. To say it another way, all of the outputs can display a single input, but each output can only display one thing at a time. When a selection is made, the selected input and output are connected as if they were wired together directly. These connections are easily changed throughout the day as the needs of the basketball game dictate.

Video Signals and Conversions

Many different video formats can carry live video down a wire. You’re probably familiar with HDMI because it has been widely adopted since its introduction. The video system discussed here primarily uses a serial digital interface (SDI), which is a digital standard for broadcast video. Hardware converters between these formats also exist, so changing between them is as easy as connecting a cable of each type. The cost for this conversion is a slight delay in the video signal that is normally not noticeable; however, if you have stacked several conversions, the delays can start to add up and become visible. This scenario generally presents itself as audio out of sync with its associated video.

The Scoreboard

In addition to team points, a scoreboard also offers other game information, such as time remaining, period of play, time outs, or player numbers. Figure 3 shows the general flow of the program. At program startup, the graphics initialize and the initial state of the scoreboard renders. Concurrently, the web server starts and the operator brings this page up on a courtside computer (Figure 4).

F3_thread-diagram.tif
Figure 3: The servers, clients, displays, and their associated libraries communicate and interact with each other.
F4_scoreboardControl.tif
Figure 4: The scorekeeper’s scoreboard interface.

As the basketball game progresses, the operator uses the web interface to update the game score and other statistics. Each action on the web interface starts as a JavaScript function that runs in the browser, communicating with the CherryPy server through background Ajax calls. When CherryPy receives these commands, it updates the graphics screen to reflect the change on the scoreboard itself and then sends the updated values back to the browser so that the operator’s interface is also updated.

Listing 1 shows some of the code behind the scoreboard. (You can download the complete code online.) To begin, you must import the external libraries the program needs: The pygame, graphics library draws the scoreboard; _thread allows multiple branches of the program to run at the same time, so you can run the scoreboard display and the web control interface at the same time; pymysql is the MySQL database library for Python; and cherrypy sets up the CherryPy framework.

Listing 1: scoreboard.py

001 import pygame
002 import _thread
003 import cherrypy
004 import pymysql
005 
006 class web:
007   def __init__ ( self , scoreboard ):
008     self.scoreboard = scoreboard
009     self.reconnect()
010     self.dbGameID = None
011 
012   def reconnect ( self ):
013     self.db = pymysql.connect(
014      host='DB_HOSTNAME',
015      user='DB_USER',
016      password = "DB_PASSWORD",
017      db='DB_NAME',
018      )
019 
020   def gameEvent ( self , event , value ):
021     if self.dbGameID == None: return
022 
023     self.db.ping ( True )
024     period = self.scoreboard.period.value
025     time = self.scoreboard.clk.seconds
026 
027     sql = "INSERT INTO `updates` ( `gameID` , `period` , `gameTime` , `event` , `value` ) VALUES ( " + str ( self.dbGameID ) + " , " + str ( period ) + " , " + str ( time ) + " , '" + event + "' , " + str ( value ) + " );"
028 
029     cursor = self.db.cursor ( pymysql.cursors.DictCursor )
030     cursor.execute ( sql )
031     self.db.commit()
032 
033   @cherrypy.expose
034   def index ( self ):
035     html = """
...
240     """.format (
241         self.scoreboard.court.value + " " + self.scoreboard.court.label,
242         self.scoreboard.scoreA.label,
243         self.scoreboard.scoreB.label,
244         self.scoreboard.scoreA.value,
245         self.scoreboard.scoreB.value,
246         self.scoreboard.clk.clockString,
247         self.scoreboard.timeoutA.value,
248         self.scoreboard.timeoutB.value,
249         self.scoreboard.posession.leftString,
250         self.scoreboard.posession.rightString,
251         self.scoreboard.period.value
252       )
253 
254     return html
255 
256   @cherrypy.expose
257   def teams ( self ):
258     cursor = self.db.cursor ( pymysql.cursors.DictCursor )
259     sql = "SELECT t1.teamName AS t1name , t2.teamName AS t2name , games.* FROM `games` LEFT JOIN `teams` t1 ON t1.ID=games.team1 LEFT JOIN `teams` t2 ON t2.ID=games.team2";
260     cursor.execute ( sql )
261 
262     html = ""
266 
267     return html
268 
269   @cherrypy.expose
270   def processGameSelect ( self , gameID ):
271     cursor = self.db.cursor ( pymysql.cursors.DictCursor )
272     sql = "SELECT t1.teamName AS t1name , t2.teamName AS t2name , games.* FROM `games` LEFT JOIN `teams` t1 ON t1.ID=games.team1 LEFT JOIN `teams` t2 ON t2.ID=games.team2 WHERE games.ID = " + str ( gameID );
273     cursor.execute ( sql )
274     game = cursor.fetchone()
275 
276     self.dbGameID = gameID
277 
278     self.teamName ( "1" , game [ "t1name" ] )
279     self.teamName ( "2" , game [ "t2name" ] )
280 
281   @cherrypy.expose
282   def teamName ( self , team , name ):
283     if team == "1": self.scoreboard.scoreA.label = name
284     if team == "2": self.scoreboard.scoreB.label = name
285 
286   @cherrypy.expose
287   def score ( self , team1 , team2 ):
288     self.scoreboard.scoreA.value += int ( team1 )
289     self.scoreboard.scoreB.value += int ( team2 )
290 
291     if team1 != "0": self.gameEvent ( "T1SCORE" , team1 )
292     if team2 != "0": self.gameEvent ( "T2SCORE" , team2 )
293 
294     self.scoreboard.render()
295     return str ( self.scoreboard.scoreA.value ) + ":" + str ( self.scoreboard.scoreB.value );
296 
297   @cherrypy.expose
298   def timeouts ( self , team1 , team2 ):
299     self.scoreboard.timeoutA.value += int ( team1 )
300     self.scoreboard.timeoutB.value += int ( team2 )
301 
302     if team1 != "0": self.gameEvent ( "T1TIMEOUT" , team1 )
303     if team2 != "0": self.gameEvent ( "T2TIMEOUT" , team2 )
304 
305     return str ( self.scoreboard.timeoutA.value ) + ":" + str ( self.scoreboard.timeoutB.value )
306 
307   @cherrypy.expose
308   def period ( self , period ):
309     self.gameEvent ( "PERIOD" , period )
310 
311     self.scoreboard.period.value += int ( period )
312     return str ( self.scoreboard.period.value )
313 
314   @cherrypy.expose
315   def getClock ( self ):
316     return self.scoreboard.clk.clockString
317 
318   @cherrypy.expose
319   def clockRun ( self ):
320     self.scoreboard.clk.running = True
321 
322   @cherrypy.expose
323   def clockStop ( self ):
324     self.scoreboard.clk.running = False
325     self.gameEvent ( "CLOCKSTOP" , 0 )
326 
327   @cherrypy.expose
328   def clockSet ( self , seconds ):
329     self.scoreboard.clk.seconds = int ( seconds ) + 1
330     self.scoreboard.clk.tick ( manual = True )
331     return self.scoreboard.clk.clockString
332 
333   @cherrypy.expose
334   def posession ( self ):
335     self.scoreboard.posession.toggle()
336     if self.scoreboard.posession.leftString == "<": self.gameEvent ( "POSESSION" , 1 )
337     if self.scoreboard.posession.rightString == ">": self.gameEvent ( "POSESSION" , 2 )
338 
339     return self.scoreboard.posession.leftString + ":" + self.scoreboard.posession.rightString
340 
341 class clock:
342   def __init__ ( self , screen ):
343     self.screen = screen
344     self.width = self.screen.get_width()
345     self.height = 250
346     self.seconds = 6 * 60
347     self.clockSurf = pygame.surface.Surface ( ( self.width , self.height ) )
348     self.clockFont = pygame.font.Font ( "open24.ttf" , self.height )
349     self.running = False
350 
351     mins = int ( float ( self.seconds ) / 60.0 )
352     secs = self.seconds % 60
353 
354     self.clockString = "{0}:{1:02d}".format ( mins , secs )
355 
356   def tick ( self , manual = False ):
357     if self.seconds > 0 and ( self.running == True or manual == True ):
358       self.seconds ‑= 1
359 
360       mins = int ( float ( self.seconds ) / 60.0 )
361       secs = self.seconds % 60
362 
363       self.clockString = "{0}:{1:02d}".format ( mins , secs )
364 
365   def render ( self ):
366     timeSurf = self.clockFont.render ( self.clockString , True , ( 0 , 255 , 0 ) )
367 
368     x = self.width / 2 ‑ int ( timeSurf.get_width() / 2 )
369     self.clockSurf.fill ( ( 0 , 0 , 0 ) )
370     self.clockSurf.blit ( timeSurf , ( x , ‑25 ) )
371     return self.clockSurf
372 
373 class posession:
374   def __init__ ( self ):
375     self.width = 300
376     self.height = 100
377     self.font = None
378     self.direction = "<"
379     self.posSurf = pygame.surface.Surface ( ( self.width , self.height ) )
380     self.leftString = ""
381     self.rightString = ""
382     self.makeSurfaces()
383 
384   def makeSurfaces ( self ):
385     self.leftSurf = self.font.render ( "<" , True , ( 255 , 0 , 0 ) )
386     self.rightSurf = self.font.render ( ">" , True , ( 255 , 0 , 0 ) )
387 
388   def toggle ( self ):
389     if self.direction == "<":
390       self.direction = ">"
391       self.leftString = ""
392       self.rightString = ">"
393     else:
394       self.direction = "<"
395       self.leftString = "<"
396       self.rightString = ""
397 
398   def render ( self ):
399     self.posSurf.fill ( ( 0 , 0 , 0 ) )
400     label = self.font.render ( "POSESSION" , True , ( 255 , 0 , 0 ) )
401     x = self.width / 2 ‑ int ( label.get_width() / 2 )
402     self.posSurf.blit ( label , ( x , 0 ) )
403 
404     if self.direction == "<": self.posSurf.blit ( self.leftSurf , ( 0 , 0 ) )
405     else: self.posSurf.blit ( self.rightSurf , ( self.width ‑ self.rightSurf.get_width() , 0 ) )
406     return self.posSurf
407 
408 class label:
409   def __init__ ( self ):
410     self.width = 250
411     self.height = 350
412     self.label = ""
413     self.value = ""
414     self.labelFont = None
415     self.valueFont = None
416     self.labelColor = ( 200 , 200 , 200 )
417     self.valueColor = ( 200 , 200 , 200 )
418     self.countSurf = pygame.surface.Surface ( ( self.width , self.height ) )
419 
420   def render ( self ):
421     self.countSurf.fill ( ( 0 , 0 , 0 ) )
422     valSurf = self.valueFont.render ( str ( self.value ) , True , self.valueColor )
423     x = self.width / 2 ‑ int ( valSurf.get_width() / 2 )
424     self.countSurf.blit ( valSurf , ( x , 0 ) )
425 
426     labelSurf = self.labelFont.render ( str ( self.label ) , True , self.labelColor )
427     x = self.width / 2 ‑ int ( labelSurf.get_width() / 2 )
428     self.countSurf.blit ( labelSurf , ( x , valSurf.get_height() ‑ 10 ) )
429 
430     return self.countSurf
431 
432 class counter:
433   def __init__ ( self ):
434     self.width = 250
435     self.height = 350
436     self.label = ""
437     self.value = 0
438     self.labelFont = None
439     self.valueFont = None
440     self.labelColor = ( 200 , 200 , 200 )
441     self.valueColor = ( 200 , 200 , 200 )
442     self.countSurf = pygame.surface.Surface ( ( self.width , self.height ) )
443 
444   def render ( self ):
445     self.countSurf.fill ( ( 0 , 0 , 0 ) )
446     valSurf = self.valueFont.render ( str ( self.value ) , True , self.valueColor )
447     x = self.width / 2 ‑ int ( valSurf.get_width() / 2 )
448     self.countSurf.blit ( valSurf , ( x , 0 ) )
449 
450     labelSurf = self.labelFont.render ( str ( self.label ) , True , self.labelColor )
451     x = self.width / 2 ‑ int ( labelSurf.get_width() / 2 )
452     self.countSurf.blit ( labelSurf , ( x , valSurf.get_height() ‑ 10 ) )
453 
454     return self.countSurf
455 
456 class board:
457   def __init__ ( self ):
458     pygame.display.init()
459     self.screen = pygame.display.set_mode ( ( 1280 , 720 ) , display = 1 , flags = pygame.FULLSCREEN )
460     pygame.font.init()
461     self.scoreFont = pygame.font.Font ( "font.ttf" , 200 )
462     self.labelFont = pygame.font.Font ( "font.ttf" , 50 )
463     self.timeoutFont = pygame.font.Font ( "font.ttf" , 50 )
464     self.posFont = pygame.font.Font ( "font2.otf" , 50 )
465 
466     self.logo = pygame.image.load ( "upwardBlack.jpg" ).convert()
467 
468     self.clk = clock ( self.screen )
469 
...

The Web Class

The web class (lines 6-339) connects to a MySQL database that will be used to retrieve team names, determine which team plays which, and record game history. When the class is instantiated, the __init__ function is called automatically to set up a few things needed later. Line 8 creates a class variable self.scoreboard and saves the reference to the scoreboard class that’s passed in as an argument.

The reconnect function is really only one call split up across multiple lines for easier readability (lines 12-18). pymysql.connect creates self.db, the local reference to the database. Each line supplies the appropriate credential.

The gameEvent function (lines 20-31) logs any change to the scoreboard. In a future version, this function will allow for recovering the scoreboard after a crash and exporting a game report. To begin, the code checks whether self.dbGameID (which was initialized to None in line 10) is set. If not, then you don’t have a database ID, so you just return.

The self.db.ping convenience function, with an argument of True, checks whether the database connection is live and, if not, reconnects automatically in the next line. The two lines that follow get the game period and time in seconds. These two values pinpoint a unique time within the game.

Line 27 builds a SQL statement. The INSERT INTO command names the table followed by a list of columns separated by commas. The VALUES keyword assigns the values for each column, provided in the same order. Line 29 creates a database cursor that interacts with an SQL statement. In this case (line 30), it just executes the SQL line created in line 27, but it has many more capabilities, especially when retrieving records. Line 31 calls commit to confirm that you want to write the data.

The cherrypy decorator (lines 33-339) creates a small web server for controlling the scoreboard. Each Python function becomes a web address that returns its designated content to the browser that has called it.

The index function acts just like index.html in a traditional web server. In the absence of another address, it is the default item returned. Most of this function is a very large multiline string (not shown here, grab the full code online) enclosed by triple quotes (""") that instructs Python to ignore any newlines or other special characters until it encounters another triple quote. The enclosed string is the HTML5 and JavaScript of the control web page. After the massive text string, the Python format command inserts all of the current variables into the web page (lines 240-251).

The @cherrypy.expose decorator tells CherryPy that it should allow this function to be reachable through its web server. Without this decorator the function remains private from anyone on the web, which allows the program to be structured with additional functions as needed, with only those specifically designed to be web-accessible published. You’ll see this decorator before each function in the web class, so it’s only described once here.

The teams function (lines 257-267) is somewhat of a misnomer in that it generates a game selector. For the purposes of this database, a game is a pair of teams at a specific date and time on a specific court. This function retrieves all of the games and returns an HTML select widget.

The function begins by creating an SQL cursor and statement and then executing it (as in lines 29-31). However, now it’s retrieving records, so the cursor has the results of the query, which is data instead of just a message that the query succeeded.

After starting the HTML select widget on line 262, the program loops over the SQL results. As the name implies, cursor.fetchall gives all of the results as an iterator. game will contain one row from the database for each time through the loop. Each pass creates an HTML option and lists the game date, time, and teams.

The processGameSelect function (lines 270-279) is called when the scoreboard operator selects a game from the drop-down just generated. It receives the gameID argument and uses that to get the game details from the database. Line 272 is an almost identical SQL statement to line 259, but with a WHERE clause added to the end with the gameID.

After doing the SQL dance one more time, line 274 calls cursor.fetchone. The last time, all of the records were retrieved, but this time only one is needed, which is saved into game. gameID is saved into a class variable, and self.teamName sets the names retrieved from the database.

The teamName function (lines 281-284) accepts a team number and name as arguments. Each of two if statements determines which team is being named and then saves the new name to a variable in the scoreboard class.

Housekeeping

The score, timeouts, and period functions (lines 286-312) accept the team1 and team2 arguments (period only accepts a period number) which are the amounts to change each value. Negative values are allowed so that values can be corrected or reset for the next game or period. Note also that the variables are preceded with int. All arguments from CherryPy come in as strings, so they have to be converted to integers before they can be used algebraically.

Finally, the two newly adjusted values are returned, separated by a colon, to go back to the JavaScript function and be split into the two scores so that the control screen is updated properly.

The JavaScript versions of these functions are generated in the index function (not included in the listing). The score function, for example, creates a JavaScript object and adds obj.team1 and obj.team2:

function score ( team1 , team2 )
{{
   obj = new Object();
   obj.team1 = team1
   obj.team2 = team2 

These are the score deltas (amounts to change) for each team. jQuery (represented by the $) then posts the object to the address score (the Python CherryPy function described above):

$.post ( "score" , obj , function ( data ) {{
    scoreParts = data.split ( ":" );
    $ ( "#team1score" ).html ( scoreParts [ 0 ] );
    $ ( "#team2score" ).html ( scoreParts [ 1 ] );
}} ); 

obj has the values to send that were set up earlier. When post finishes and receives a response, function ( data ) is called, where data is the returned string with the newly updated scores (or timeouts or period). First, the data is split into its two parts with data.split, and then the score on the control page is updated with jQuery.

Time and Possession

The clock is a unique case because it is the only game variable that updates independently. Everything else is a response to something that happens on the court.

In the clock function (lines 314-331), getClock returns a string that represents the current game time, called by a recurring interval in a JavaScript function on the operator’s web page to update the operator’s clock. A timer event in the graphics thread both updates the internal clock variable and redraws the clock on the scoreboard display.

updateClock is only found in the JavaScript and does the job of keeping the control page clock up to date. It calls getClock by jQuery to get the current clock time then updates the HTML div with the current value:

function updateClock()
{{
   $.post ( "getClock" , function ( data ) {{
      $ ( "#clock" ).html ( data );
   }} );
}}

Both the clockRun and clockStop functions change the state of self.scoreboard.clk.running. If that variable is True, the clock is updated. If it’s False, the clock is not updated or is, for all practical applications, paused. clockStop also calls self.gameEvent to add an entry that the clock was stopped. Because the clock is not team-specific, the second argument is 0. The JavaScript version of these functions (lines 80-92) are not shown. Once the Python function is called by jQuery, a JavaScript interval is either set or cleared to update the control page.

For the clockSet function, the variable self.scoreboard.clk.seconds keeps track of the game time remaining in seconds, which is set to one second more than requested. Next, self.scoreboard.clk.tick automatically subtracts one second. Passing manual = True forces a redraw of the clock even though it is not running. Finally, self.scoreboard.clk.clockString, which is a text representation of the time remaining in minutes and seconds, returns.

The JavaScript version can be found on lines 101-116 (not shown). It asks the operator what the clock should be set to and will contain either the requested time or null if the dialog box was canceled.

If the value is valid, the request is split into minutes and seconds and then converted to seconds. After a new object is created and a seconds parameter added, it is posted by jQuery back to the Python function. The control screen then updates with the new clock value and the update interval clears so clock requests are not made until it starts running again.

The posession [sic] function (333-339) calls the toggle method of self.scoreboard.posession, which flips which team currently has the ball (indicated by the < and > symbols as arrows).

Lines 336 and 337 check to see which team has possession and add a gameEvent to reflect the change. Finally, two strings are returned separated by a colon; each string is either a space or one of the < or > symbols. The JavaScript version is on lines 71-78 (not shown). After posting a possession change by jQuery the return string is split into two parts, and the left and right divs are updated on the control screen.

The Clock Class

The clock class (lines 341-371) manages the game clock for the main graphics (scoreboard) display. The __init__ function sets up a few things: self.screen is a reference to the screen object passed when creating the class; self.width stores the screen width by calling get_width() on the screen surface; self.height is set explicitly to define how tall you want the clock to be displayed; and self.seconds is the time on the clock, initialized to 6 * 60 seconds to reflect six-minute periods. Line 347 creates a Pygame surface onto which the clock is drawn, and line 348 sets up the font. Finally self.running tracks whether the clock is running or paused.

Lines 351-354 calculate the clock values and convert to int; seconds is the mod (remainder) of self.seconds / 60. Finally, self.clockString is assembled from the calculated values.

The main loop calls the tick function (lines 356-363) once a second. If time is left on the clock and either self.running or manual is set to True, self.seconds decrements by one, and the same calculations as before are repeated to update self.clockString. The manual parameter used when setting the clock forces the clock to be regenerated immediately rather than waiting for the next automatic call to tick.

The render function (lines 365-371) draws the clock. self.clockFont.render takes self.clockString and a color tuple as arguments. The True argument says to anti-alias the text as it is drawn. The result is timeSurf, a Pygame surface with the current game time drawn onto it.

After the center point of timeSurf is calculated, self.clockSurf is cleared by filling it with black and blitting (copying) timeSurf onto it. Then the surface is returned. This copying might seem like an extra step, but here’s what’s happening: When self.clockFont.render generates the text, the surface is the exact size of the text it generated. For it to fill the space at the dimensions specified in __init__, you have to copy it onto the larger surface.

The Posession Class

The posession class (lines 373-406) indicates who has the ball at any given time. __init__ ( self ) sets up the graphical display, with the initial state specified by self.direction (who currently has the ball). A surface is created and initialized with self.leftString and self.rightString.

The convenience function makeSurfaces creates self.leftSurf and self.rightSurf possession arrows rendered with < and > symbols.

When the toggle function (lines 388-396) is called, possession has changed from one team to the other. The if checks to see which team currently has possession and then updates all of the variables to their opposite states. Note that self.leftString and self.rightString either have an arrow or an empty string.

When everything is ready to be drawn in the render function, self.posSurf is cleared by filling it with black (line 399) before generating the label text, calculating the center point, and blitting it onto the final surface.

Lines 404 and 405 make sure the arrow is blitted on the appropriate side of the surface. If self.direction is <, the arrow is drawn on the left side of the surface; otherwise, it’s drawn on the right before the final surface is returned.

Labels and Counters

The label class (lines 408-430) represents an arbitrarily labeled value on the scoreboard. The label and its value can have separate fonts and colors. The Pygame surface is created on line 418.

The render function starts by clearing the surface (fill) with black then rendering the value, calculating its center point, and drawing it. Lines 426-428 do the same thing for the label text before returning on line 430.

Lines 432-454 describe the counter class. Counters are identical to labels, except their self.value is expected to be an integer.

The Board Class

Everything comes together in the board class (lines 456-538). All of the modules that have been defined to this point are initialized here, starting with pygame.display.init, which starts the Pygame engine. The pygame.display.set_mode creates the drawing surface, setting the window size. display = 1 makes the Pygame window default to the second monitor, and flags = pygame.FULLSCREEN is self-descriptive.

Because this program uses Pygame’s font capabilities, these are initialized on line 460, after which all of the fonts are loaded. In Pygame a font is the face itself and an associated size, so the same font file may be loaded multiple times with different sizes.

Line 466 loads self.logo, which is the logo displayed along the bottom of the scoreboard. The convert at the end of the line makes the image’s internal format match Pygame’s initialized display format so the logo blit onto the screen is faster.

Line 468 creates an instance of the clock class, with a reference to self.screen passed in so that it can draw the updated clock directly.

The final lines of the program (not shown) initialize five different instances of the counter class for the home and visitor’s scores, number of timeouts for each team, and the game period. In each case, valueFont and labelFont are set to one of the font/size pairs initialized earlier. A label indicates which court each scoreboard is tracking.

A Typical Game Day

When the crew arrives on game day, the first task is to set up the scoreboard operator’s console (two Raspberry Pis, one per scoreboard). Once that’s done, robotically controlled cameras are placed around the court and everything is tested. The youngest teams play first in a half-court model. Halfway through the day, play switches to full-court games (Figure 5).

F5_court-layouts.tif
Figure 5: Depending on the age group playing, the game may be on full or half courts. Each scoreboard must be routed to follow the current court configuration.

At halftime of each game, video messages play onto the screens, and the scoreboard returns to normal operation (Figure 6). At the halfway point of the day one scoreboard system is routed to both score displays because a single court is now in use. This switching is accomplished with a matrix video router. 

F6_scoreboardDisplay.tif
Figure 6: The scoreboard display as seen on the large, audience-facing monitors.

Controlling a Video Matrix

Although the basic operation of a matrix router is fairly simple to understand, it’s always easier to have meaningful names and labels attached instead of remembering sets of numbers. Adding some shortcuts to common configurations makes things even easier, which is the purpose of the matrix control program (not shown, but available online).

The Raspberry Pi communicates with the matrix over a USB serial connection set up in Python:

class matrixSerial:
  def __init__ ( self ):
    self.port = serial.Serial ( "/dev/ttyUSB0" , 9600 )
 
  def send ( self , cmd ):
    self.port.write ( cmd )

The __init__ line creates self.port, which is an instance of the Serial object with the name of the serial device and the requested baud rate. The defaults are fine for everything else. The send function takes in cmd and transmits it with a call to self.port.write, thus sending data to the matrix.

The Button Class

The main function of the program is to present easy options to the user on a touchscreen; a button class sets up the actions to button presses. When the class is instantiated, it receives screen, which is a reference to the Pygame screen object, and font, which is a Pygame font object to be used to label the buttons. Each of these is stored in class properties for use by other methods.

When the create function is called, it receives a number of variables to get started, including the x and y screen coordinates where the button should be drawn, the width and height of the button, and the label text displayed on the button. self.rect stores the Pygame rect object, which has functions to check whether coordinates are within its bounds, along with several other useful utilities, and self.matrixCommands and self.osCommands are the actions taken when the button is pressed.

The addOsCommand function takes its associated string argument and passes it to the operating system when the button is pressed:

def addOsCommand ( self , cmd ):
  self.osCommands.append ( cmd )

The addMatrixCommand function is the same, except it transmits to the matrix.

The render function is responsible for drawing the button on the screen. As you have seen before, it creates a surface and fills it – with green in this case. If its label is not empty, the program renders the text, calculates how to center the text, and adds the text to the button blit. The surface is then drawn to the screen with a reference saved to its Pygame rect.

The Matrix Class

The matrix class draws and manages the interface on the touchscreen. As usual, an __init__ function initializes the different components of the interface, but this example has a lot of them. The graphics system starts and creates a window to draw on. Although the window has a caption, it is generally not visible because the window is in fullscreen mode.

As usual, the fonts and serial communications are initialized, although pygame.font.SysFont has an empty string as its first argument, thus asking Pygame for the default font:

buttonFont = pygame.font.SysFont ( "" , 32 )
self.matrixPort = matrixSerial()

The next line creates self.buttons as a list, which is where each of the buttons added to the interface are stored. Most of the rest of the init function creates each of the buttons and defines their functionality. They all follow the general format:

btn = button (self.screen, buttonFont)
btn.create ( <x> , <y> , <width> , <height , "<label" )
btn.addMatrixCommand ( chr ( 0x05 ) + chr ( 0x55 ) + chr ( 0x19 ) + chr ( 0x00 ) )
btn.addMatrixCommand ( chr ( 0x05 ) + chr ( 0x55 ) + chr ( 0x19 ) + chr ( 0x11 ) )
self.buttons.append ( btn ) 

The first line creates a button and passes in self.screen and buttonFont. The create makes the button, and addMatrixCommand adds commands for the video matrix. The command string comes from the matrix documentation; it tells you to send the first three hex values to start the command. The last byte contains the routing as 0 indexed addresses. The high-order digit defines the output or destination, and the low-order digit defines the input or source. Here, 0x00 says “connect the first output to the first input.” The next line connects the second output to the second input. Table 1 shows the configuration for each button.

Table 1: Buttons and Configurations

Line Nos.

Button

Action

57-61

Separate Scoreboards

Each display shows its own scoreboard computer.

63-67

Single Scoreboard

Both screens display the first scoreboard computer.

69-73

Both Program

Both screens display the video switcher program output.

75-78

Court A Program

Switch only the court A screen to the video switcher output.

80-83

Court B Program

Switch only the court B screen to the video switcher output.

85-88

Court A Scoreboard A

Switch only the court A screen to its scoreboard computer.

90-93

Court B Scoreboard B

Switch only the court B screen to its scoreboard computer.

95-98

CR Gym 1 Action

View Scoreboard (NUC) 1 on the control room monitor.

100-103

CR Gym 2 Action

View Scoreboard (NUC) 2 on the control room monitor.

105-108

CR Program Action

View the video switcher program output on the control room monitor.

110-113

Record

Start recording onto an SD card or flash drive.

115-118

Stop

Stop recording.

One output of the matrix is connected to a monitor in the control room so that the sources can be easily monitored. The three CR buttons allow any of the matrix sources to be viewed on the control room monitor (Figure 7).

F7_switcherMultiview.tif
Figure 7: The switcher multiview shows all of the cameras and other sources available. They can be put “on air” by clicking on them.

All of the buttons except the last two use addMatrixCommand. The Record and Stop buttons use addOsCommand instead and then use wget to trick the digital recorder into thinking the buttons on its web GUI are being pressed to start and stop the recording onto an SD card or flash drive (Listing 2).

Listing 2: wget Trick

btn.addOsCommand ( r'wget ‑‑header="Accept:*/*" ‑‑header="Accept‑Encoding: gzip, deflate" ‑‑header="Accept‑Language: en‑US,en;q=0.9" ‑‑header="Connection: keep‑alive" ‑‑header="Cookie: serenity‑session=72952074" ‑‑user‑agent="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "http://172.16.2.20/cgi‑bin/system.cgi?command=recording&action=start"' )

The render function iterates over self.buttons, calling each render method and creating all of the buttons onscreen, as well as all the Pygame rect objects to see whether they’ve been clicked. Once everything has been drawn to the buffer, the function calls pygame.display.flip to make it visible in the drawing window.

The loop function makes everything interactive by watching for buttons presses, sending commands when they are, and keeping up with everything that’s happening.

The looping value is first set to True, before the function enters the while loop with looping as its argument. If looping is set at any point to False, the loop exits.

The function then waits for Pygame events and checks to see if the event is a button click. To see if any of the buttons have been pressed, the function loops over self.buttons again and gets the coordinates of the mouse click: 

for btn in self.buttons:
  if btn.rect.collidepoint ( event.pos ) == True:
    for cmd in btn.matrixCommands:
      self.matrixPort.send ( cmd + chr ( 0x77 ) )

If True, a button has been pressed. To make the button do its thing, the program loops over btn.matrixCommands and sends each string to the matrix. To give the matrix time to process each command, the function waits half a second before moving on.

The lines that follow do the same thing for any operating system commands (btn.osCommands) but pass them to os.system instead. The call to matrix on the last line of the program sets everything in motion.

Conclusion

I hope this article has given you some insight into a different type of network. When you think about the idea that similar control rooms like this exist all over the world and they each feed their own set of displays or a broadcast video system network, you can see the similarities and differences between the Internet and IP routing. You’ve also seen how these scoreboards are generated and controlled to support a local basketball league. Now that all the games and the season are done, it’s time to hit the showers.