In the lab, it is common to find different computers connected to specific devices. For example,when you keep older PCs which are able to communicate with very specific hardware. You may also have different computers when there are mobile instruments that you share among different users. In these situations it becomes very useful to be able to exchange information between your main computer and a secondary one.
A computer network utilizes two elements: a server and a client. The server receives the messages you send, interprets them and returns values if asked to. The client communicates with the server, sends commands and receives data. Internet works this way: when you entered this website, you used a browser, the client, to access content on a server. Communicating with a device connected to another computer is, therefore, not different from what we have just described.
If you look around for Python frameworks to build web applications, you will find several but two are going to stand out: Django and Flask. Django is a complete package for developing web applications, but a total overkill for our purposes. Flask is slightly more barebones, but it provides all the functionality that we are looking for: to create a server that will take inbound communication and act accordingly, for example by triggering a measurement on a device.
Installing Flask doesn't take much more than a pip command:
pip install Flask
After that we can build our first very simple app to see that everything is working fine:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "The Server is up and running"
app.run()
If you run the file, open a browser, and head to localhost:5000
you
will see a message saying that the server is up and running. A lot of
things are happening that are worth discussing in detail. We start by
importing Flask and creating an app
. Our app is very powerful; one of
the things it allows us to do is to trigger specific functions when we
head to specific locations on the server. These are called routes
;
when we define a route and we add the string '/'
it means that the
function following it will be triggered whenever someone enters to the
root of the server (/
). Our example only returns a message saying that
everything went well.
The last line runs the app. This is an infinite loop that will open a
port at localhost for us to test our application. If you are familiar
with how PyQt works, you will notice the similarities. Once the app is
running, you can point the browser to the address that appears on your
command line, most likely localhost:5000
. When you enter to that
address, you are triggering the route('/')
and therefore you will get
the message The Server is up and running
.
So far, both the server and the client are the same device. We will see
later how to improve on this, but for the time being, you can believe
that everything will work exactly the same, even when communicating
through the network. It is possible to trigger other actions directly on
the server side, not only to return strings. To test it, we can use a
print
statement. Let's re-write the index()
function:
[...]
def index():
print('Index Triggered')
return 'The server is up, running, and printing statements'
[...]
If you run the server file again and head your browser to
localhost:5000
you should see not only the string appearing on your
screen, but also a message will appear on the command line where the
server is running. The print
function is being triggered on the
server. We could use more complex functions than print
. For example,
we could trigger a measurement on a device.
Note
If you are not dealing with instruments but you would like to trigger computer-intensive tasks on a remote computer, you can use the same approach explained here. You can then leverage computers with more memory or better processors, or you can even make a parallel execution of your code without leaving your Jupyter notebook.
Let's assume you have a device like the one we developed in our earlier
post How to Write a Driver with
Lantz. The device is an
oscilloscope with several built-in methods, including idn
for getting
its serial number, and datasource
to set and get the channel used for
an acquisition. We would like to trigger some of those methods when we
head to specific addresses on our browser. Later on, we will change the
browser by a custom-made client that will simplify our workflow. We
begin by initializing the device and we make it available to the app:
from flask import Flask
from devices import my_device
dev = my_device.via_usb()
app = Flask(__name__)
@app.route('/idn')
def idn():
return dev.idn
app.run()
The core is the same as before, but we have added some lines for the
device. We import the needed classes and we initialize the communication
with the device; you should adapt the highlighted lines with your own
device. The new route now establishes that if you head to
localhost:5000/idn
, the serial number of the device is going to be
returned. This action is much more complex than printing on the server
or returning a simple string. What we are actually sending is a command
to a device, waiting for it to return a value and then we are sending it
back to the browser. With this simple example, you can already see that
we are doing virtually everything that a device can handle. Of course,
devices also take inputs, and we should take into account this. Basing
ourselves on the example of an oscilloscope with
Lantz, we could change the
datasource property of the device like this:
[...]
@app.route("/datasource/<int:source_id>")
def datasource(source_id):
dev.datasource = source_id
return(dev.datasource)
The lines above show a very simple way of sending variables through a
browser. The route
takes more complex structures than plain strings.
<int:source_id>
will take an integer after the datasource/
and it
will pass it as an argument to the function below. The function
datasource
in our server, therefore, should take exactly one argument,
source_id
, and we use it for changing the datasource
of the device.
Now, if you head your browser to localhost:5000/datasource/1
we will
change the source to 1, we can do the same with 2, 3, etc. Bear in mind
that not all values are valid with the device. Check what happens if,
for example, you send a value outside the range of what is possible.
Communicating with our devices through the browser may not be the most
practical approach. Instead, we can build a special program called
Client that will handle the sending and retrieving of information from
the server. When we have control on both the server and the client side
software, we can easily control the data that is being exchanged. When
we don't have control over one of the two sides, we have to base
ourselves on available standards; for example, the data that a browser
can handle is limited, the instructions a server can receive are few,
etc. We are going to base our client on a common Python library called
requests
:
import requests
addr = 'http://localhost:5000'
r = requests.get(addr + "/idn")
print(r.content)
If you run the script written above (while the server script is running on a different command line), you will see that what gets printed on screen is the identification of the device. Basically, what you have achieved is the exchange of information from a device hooked to a server with a client not directly bound to that device. You could build a class around the requests. If you want, for example, a client exclusively for the oscilloscope, we can do the following:
import requests
def ClientOscilloscope():
def __init__(self, addr):
self.addr = addr
def idn(self):
r = requests.get(self.addr + '/idn')
return r.content
if __name__ == '__main__':
c = ClientOscilloscope('http://localhost:5000')
print(c.idn())
The applications of this approach are multiple and not limited to
communicating over the network. Imagine that you want to share the
information of a device with multiple applications; instead of
initializing the communication with the device in each application (that
will almost certainly lead to issues), you can communicate through a
server, even if on the same computer. You can test this idea if you
access localhost
from two different browsers. You can get the idn
of
your device twice without issues. You can also run the client script
from two different command lines, and you will see that your server can
handle several requests at the same time without issues and without
blocking the device; the communication is initialized only once, at the
beginning of the server script.
Being able to access the server from a different computer depends on the
configuration of your network. First, you need to know the ip
address
of your computer. Remember that an ip is a unique number that identifies
your connection to a network; if you are connected to the Internet, you
will have two different numbers, the ip of your computer within a local
network, and the public ip that is going to be shared by all the other
computers on the same network.
Let's assume that you want to control a device within a local network in your lab. The only thing you need to do is to run the server on the computer you wish to use; most likely you are going to desire a specific port number for the inbound communication. You can do so with this simple command:
app.run('0.0.0.0', 1234)
which will allow you to run the server on port 1234. You have to check
that the port is not used by other processes; for example, port 80 is
used by HTTP connections. You can aim for higher numbers like 10000 and
above, since those are most likely not used and open within your
network. If you now head the browser of another device to ip:1234/idn
you should see the identification number of your device. This procedure
is mobile-friendly; you could use your phone to trigger measurements,
without developing any apps, just using your mobile browser.
Accessing a computer from outside the local network is possible, but it
normally depends on the policy of the institution where you work. The
easiest way is to have port forwarding, for example when you access
public_ip:specific_port
, the connection is forwarded to a specific
computer within the local network. To configure it, you need help from
the administrator of the network and as a general safety rule, they will
never allow such a thing. If you make a mistake, you are giving access
to anyone who finds out which port to use.
The possibilities are limitless. If you want to see how to configure a
more complex Server/Client strategy that handles any number of devices,
you can check Uetke's Instrument
Server. In this project, the server
is an extension of Flask; we have defined some common routes to
communicate with clients. We have also made use of JSON
as a way of
exchanging structured information between client and server. The
repository also includes a client and a fake instrument to test the
behavior.
The examples we have shown above are very basic but important to understand, if you want to achieve more complex functionality. For example, if you want the server to stay responsive while triggering tasks that take long to execute on a device, you have to implement threads. That is a more extensive discussion than what we can have here, but you can find an implementation example here. There are some other packages that can be used for threading on web servers. Those packages were created precisely to handle async tasks. They are aimed at web development but could be useful also for applications with experiments. You can check for example, Celery and RabbitMQ, although they are fairly complex, they can be exactly what you are looking for.
If you need help developing a code for communicating over the network, don't hesitate to contact us. We can custom build a solution to your problem. If you would like to learn about network communication and much more, you can also consider our Advanced Python For The Lab Course.
Header photo by John Carlisle on Unsplash