Pyro – Networking made simple
Converting Python to Pyro
In many cases, Python classes you've already written can be converted directly to Pyro objects just by adding a few decorators. Decorators are Python's way of extending the functionality of a class or function without explicitly modifying it. The following are some of the most common Pyro decorators on the class:
@Pyro4.behavior(instance_mode="single")
: Tells Pyro to use a single instance of the class regardless of how many controllers connect to the daemon. When you're dealing with a finite resource (displays, bells, pixels), this mode helps prevent resource conflicts. You can also specifypercall
for the instance mode, which creates a new version of the daemon for every call, regardless of where it originates. Once the call is finished, the instance is discarded. If you don't specify an instance mode, Pyro usessession
. A new instance of the daemon will be created for each unique controller that connects. Each controller can see changes it has made to its specific daemon, but multiple controllers will not share data.@Pyro4.expose
: Exposes an existing class to the Pyro network. When put above a class definition, every class method and property is then available to any controller that connects, which is handy if you can't modify the class itself; however, it can also introduce some security holes.
Common Pyro decorators for functions include:
@Pyro4.expose
: Exposing specific methods inside classes is the preferred method of making class methods available to the Pyro network. Pyro handles the movement of all of the data across the network, and you call the method as if it were running locally.@Pyro4.oneway
: Tells Pyro to return immediately rather than wait for a response, which is preferred if you're going to do something that takes a long time. The downside is that you can't return a value to the controller. Any calls to this method return immediately to the controller, and the process is handled in its own thread on the daemon.@property
: Tells Pyro to treat the method as a read-only property. In this case, it is accessible throughremoteObject.timeValue
:
@property def timeValue ( self ): return self.value
attr.setter
: Tells Pyro to treat the method as a writable property:
@attr.setter def editableTime ( self , newValue ): self.value = newValue
In this case, remoteObject.editableTime = 25
.
Colorsquare Example
The Colorsquare example has two parts: a transmitter and a receiver. I'll look at receive.py
(Listing 1) first. Pyro is imported just like any other library, and I import PyGame
and time
, as well (lines 3-5).
Listing 1
receive.py
Lines 8-16 initialize the Pyro class. The first Pyro decorator in line 8 tells Pyro to create a single
instance of the class and use it for all requests. If you were to leave this out, each controller that connected to the class would spawn a separate instance of the class.
Line 11 defines the __init__
function, which works as expected, with one caveat – it is only called when something connects to the Pyro object. If nothing ever connects, then __init__
never runs. Lines 13 and 14 initialize PyGame, which draws the square onscreen, and line 16 stores the start time of the process, so run time can be calculated later.
The Pyro function appears in lines 22-28. @Pyro4.expose
in line 22 tells Pyro that this function can be called by a connected controller. Class methods without this decorator won't be accessible with Pyro calls.
Lines 24 and 25 change the color of the square on the screen and then redraw the display so the new color is visible. As with most functions, return
is found at the end (line 28). Pyro takes care of getting the return value back across the network to wherever the call initiated.
The other functions in lines 30-34, 36-40, and 42-46 all work exactly the same way. The only difference is a hard-coded color value. The getTime
in lines 48-50 doesn't change the display; it simply returns how long the daemon has been running.
Now the code sets up Pyro and goes from just a class to a remote object (lines 52-61). To start, line 53 creates a daemon; then, the class is registered with the daemon (line 55) and returns its own URI. You can then save the URI in a database, write it to a temp file, or (as done here) use it to register with the name server. Line 57 finds the name server and line 59 registers it and lets it know how the service should be known. Now you can start listening and respond to calls as they arrive (line 61).
Setting up the controller is even easier (Listing 2). To begin, import Pyro (line 1) and find the name server (line 4), as in the receiver code. Next, search for the daemon to which you want to connect (line 6). Finally, create a proxy to the remote object (line 8).
Listing 2
transmit.py
These steps can be repeated as needed to connect to as many daemons as you like. Afterward, you can treat everything as if it is local.
The rest of the script asks for the user input (line 13) described earlier (Figure 3, bottom right) and then processes it. If it is a color known by name, the script calls that color's function (lines 16-18). If TIME
has been requested, it is calculated and returned (line 19); otherwise, the program tries to parse the input as three comma-separated numbers (lines 22-26). The try
/except
exception handler (lines 22-25) will display a message if it can't find three numbers separated by commas.
Scoreboards
Each scoreboard runs identical code, with the exception of the Pyro name registered with the name server. To start the program on each Rasp Pi, I logged in via SSH from my desktop and started the Python script directly from the terminal. Each Rasp Pi had a separate tab, so any problems with connectivity or other errors could be checked from the console to see what's happening. My desktop was running the Python name server and controller, as well.
Each scoreboard had three major functions: updating the team name, updating the score, and displaying a timer. Once started, the timer mode was exited by pushing a stop button. Each button was wired to the Rasp Pi's GPIO via twisted pair wire. A simple header was connected to GPIO14 (physical pin 8, transmit) [2], which was selected because of the adjacent ground connection.
In the code walkthrough, I'll focus on the Pyro functions, although the majority of the code is actually for the graphics. As with any Python program, you have to start by importing libraries (Listing 3). In this case I import Pyro, pygame
, time
, and RPi.GPIO
. Next, I set the mode of the GPIO pin numbering to the Broadcom SoC channel number, not the pin number on the board. Although I'm only using one GPIO pin, this is the pin numbering I'm used to.
Listing 3
display.py
The @Pyro4.behavior
decorator (line 8) sets the instance_mode
to single
, so the class will be instantiated once, and all calls will use the same instance.
The __init__
method is standard, but remember that Pyro will not instantiate a class until it is actually called from a controller, which can make your first call seem a little sluggish as everything gets set up. Response times will improve afterward. As mentioned before, if nothing ever connects to the associated Python object, then the __init__
method will not be called.
Lines 11, 14, 19, 23, and 40 print status messages that I can monitor from the terminal from which I launch each daemon (Figure 4). Lines 12 and 13 initialize PyGame and set up a window in which to draw. The pygame.FULLSCREEN
removes all window decoration and maximizes the window to take over the entire desktop.
Next, I set up the fonts (lines 16-18); The init()
starts the PyGame font module, and then pygame.font.Font
loads a font from disk. The second argument specifies the font size in pixels. Each instance of a font object has the font itself and the size it should be rendered. If I want to use the same font in different sizes, I have to initialize it multiple times.
To set up the background image (lines 21-23), I load it from disk then add .convert()
to the end to store it internally in the same format as my initialized display; self.screen.blit
then draws it on the display.
The next group of lines that begin with self
set default values for scores, team names, drawing color, and timers, with the timer status. I also set up some variables for blitting the background back onto itself to erase portions of the display.
The next lines (37-38) set up the GPIO – in this case, one input pin. The pull_up_down
entry gives the input pin the electronic equivalent of a default value. In the absence of a signal, the pull up will make the pin read 1
, or HIGH. The add_event_detect
sets up a trigger so that when the GPIO pin falls (i.e., a button is pressed), a class method is called (self.bumpPoints
). The timer functions will read the GPIO directly, but this allows calling the method in the background.
On the last day of camp, we introduced an Easter Egg into the scoreboard program. The buttons along the front of the stage allowed campers to add points to their team total. Once discovered, campers staged elaborate plans to keep pressing their buttons to run the scores up. Internally, these button presses call self.updateScore
(line 43), awarding the team 10 points with each press.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
So Long Neofetch and Thanks for the Info
Today is a day that every Linux user who enjoys bragging about their system(s) will mourn, as Neofetch has come to an end.
-
Ubuntu 24.04 Comes with a “Flaw"
If you're thinking you might want to upgrade from your current Ubuntu release to the latest, there's something you might want to consider before doing so.
-
Canonical Releases Ubuntu 24.04
After a brief pause because of the XZ vulnerability, Ubuntu 24.04 is now available for install.
-
Linux Servers Targeted by Akira Ransomware
A group of bad actors who have already extorted $42 million have their sights set on the Linux platform.
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.
-
Kodi 21.0 Open-Source Entertainment Hub Released
After a year of development, the award-winning Kodi cross-platform, media center software is now available with many new additions and improvements.
-
Linux Usage Increases in Two Key Areas
If market share is your thing, you'll be happy to know that Linux is on the rise in two areas that, if they keep climbing, could have serious meaning for Linux's future.
-
Vulnerability Discovered in xz Libraries
An urgent alert for Fedora 40 has been posted and users should pay attention.