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
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.