1
###############################################################################
2
#    pyrpl - DSP servo controller for quantum optics with the RedPitaya
3
#    Copyright (C) 2014-2016  Leonhard Neuhaus  (neuhaus@spectro.jussieu.fr)
4
#
5
#    This program is free software: you can redistribute it and/or modify
6
#    it under the terms of the GNU General Public License as published by
7
#    the Free Software Foundation, either version 3 of the License, or
8
#    (at your option) any later version.
9
#
10
#    This program is distributed in the hope that it will be useful,
11
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
12
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
#    GNU General Public License for more details.
14
#
15
#    You should have received a copy of the GNU General Public License
16
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
###############################################################################
18

19 3
from . import redpitaya_client
20 3
from . import hardware_modules as rp
21 3
from .sshshell import SshShell
22 3
from .pyrpl_utils import get_unique_name_list_from_class_list, update_with_typeconversion
23 3
from .memory import MemoryTree
24 3
from .errors import ExpectedPyrplError
25 3
from .widgets.startup_widget import HostnameSelectorWidget
26

27 3
import logging
28 3
import os
29 3
import random
30 3
import socket
31 3
from time import sleep
32 3
import numpy as np
33

34 3
from paramiko import SSHException
35 3
from scp import SCPClient, SCPException
36 3
from collections import OrderedDict
37

38
# input is the wrong function in python 2
39 3
try:
40 3
    raw_input
41 2
except NameError:  # Python 3
42 2
    raw_input = input
43

44
# default parameters for redpitaya object creation
45 3
defaultparameters = dict(
46
    hostname='', #'192.168.1.100', # the ip or hostname of the board, '' triggers gui
47
    port=2222,  # port for PyRPL datacommunication
48
    sshport=22,  # port of ssh server - default 22
49
    user='root',
50
    password='root',
51
    delay=0.05,  # delay between ssh commands - console is too slow otherwise
52
    autostart=True,  # autostart the client?
53
    reloadserver=False,  # reinstall the server at startup if not necessary?
54
    reloadfpga=True,  # reload the fpga bitfile at startup?
55
    serverbinfilename='fpga.bin',  # name of the binfile on the server
56
    serverdirname = "//opt//pyrpl//",  # server directory for server app and bitfile
57
    leds_off=True,  # turn off all GPIO lets at startup (improves analog performance)
58
    frequency_correction=1.0,  # actual FPGA frequency is 125 MHz * frequency_correction
59
    timeout=1,  # timeout in seconds for ssh communication
60
    monitor_server_name='monitor_server',  # name of the server program on redpitaya
61
    silence_env=False)  # suppress all environment variables that may override the configuration?
62

63

64 3
class RedPitaya(object):
65 3
    cls_modules = [rp.HK, rp.AMS, rp.Scope, rp.Sampler, rp.Asg0, rp.Asg1] + \
66
                  [rp.Pwm] * 2 + [rp.Iq] * 3 + [rp.Pid] * 3 + [rp.Trig] + [ rp.IIR]
67

68 3
    def __init__(self, config=None,  # configfile is needed to store parameters. None simulates one
69
                 **kwargs):
70
        """ this class provides the basic interface to the redpitaya board
71

72
        The constructor installs and starts the communication interface on the RedPitaya
73
        at 'hostname' that allows remote control and readout
74

75
        'config' is the config file or MemoryTree of the config file. All keyword arguments
76
        may be specified in the branch 'redpitaya' of this config file. Alternatively,
77
        they can be overwritten by keyword arguments at the function call.
78

79
        'config=None' specifies that no persistent config file is saved on the disc.
80

81
        Possible keyword arguments and their defaults are:
82
            hostname='192.168.1.100', # the ip or hostname of the board
83
            port=2222,  # port for PyRPL datacommunication
84
            sshport=22,  # port of ssh server - default 22
85
            user='root',
86
            password='root',
87
            delay=0.05,  # delay between ssh commands - console is too slow otherwise
88
            autostart=True,  # autostart the client?
89
            reloadserver=False,  # reinstall the server at startup if not necessary?
90
            reloadfpga=True,  # reload the fpga bitfile at startup?
91
            filename='fpga//red_pitaya.bin',  # name of the bitfile for the fpga, None is default file
92
            serverbinfilename='fpga.bin',  # name of the binfile on the server
93
            serverdirname = "//opt//pyrpl//",  # server directory for server app and bitfile
94
            leds_off=True,  # turn off all GPIO lets at startup (improves analog performance)
95
            frequency_correction=1.0,  # actual FPGA frequency is 125 MHz * frequency_correction
96
            timeout=3,  # timeout in seconds for ssh communication
97
            monitor_server_name='monitor_server',  # name of the server program on redpitaya
98
            silence_env=False)  # suppress all environment variables that may override the configuration?
99

100
        if you are experiencing problems, try to increase delay, or try
101
        logging.getLogger().setLevel(logging.DEBUG)"""
102 3
        self.logger = logging.getLogger(name=__name__)
103
        #self.license()
104
        # make or retrieve the config file
105 3
        if isinstance(config, MemoryTree):
106 3
            self.c = config
107
        else:
108 3
            self.c = MemoryTree(config)
109
        # get the parameters right (in order of increasing priority):
110
        # first defaults, then environment variables, config file, and command
111
        # line arguments
112 3
        self.parameters = defaultparameters
113
        # get parameters from os.environment variables
114 3
        if not self.parameters['silence_env']:
115 3
            for k in self.parameters.keys():
116 3
                if "REDPITAYA_"+k.upper() in os.environ:
117 3
                    newvalue = os.environ["REDPITAYA_"+k.upper()]
118 3
                    oldvalue = self.parameters[k]
119 3
                    self.parameters[k] = type(oldvalue)(newvalue)
120 3
                    if k == "password": # do not show the password on the screen
121 3
                        oldvalue = "********"
122 3
                        newvalue = "********"
123 3
                    self.logger.debug("Variable %s with value %s overwritten "
124
                                      "by environment variable REDPITAYA_%s "
125
                                      "with value %s. Use argument "
126
                                      "'silence_env=True' if this is not "
127
                                      "desired!",
128
                                      k, oldvalue, k.upper(), newvalue)
129
        # settings from config file
130 3
        try:
131 3
            update_with_typeconversion(self.parameters, self.c._get_or_create('redpitaya')._data)
132 0
        except BaseException as e:
133 0
            self.logger.warning("An error occured during the loading of your "
134
                                "Red Pitaya settings from the config file: %s",
135
                                e)
136
        # settings from class initialisation
137 3
        update_with_typeconversion(self.parameters, kwargs)
138
        # get connection settings from gui/command line if missing
139 3
        if self.parameters['hostname'] is None or self.parameters['hostname']=='':
140 0
            gui = 'gui' not in self.c._keys() or self.c.gui
141 0
            if gui:
142 0
                self.logger.info("Please choose the hostname of "
143
                                 "your Red Pitaya in the hostname "
144
                                 "selector window!")
145 0
                startup_widget = HostnameSelectorWidget()
146 0
                hostname_kwds = startup_widget.get_kwds()
147
            else:
148 0
                hostname = raw_input('Enter hostname [192.168.1.100]: ')
149 0
                hostname = '192.168.1.100' if hostname == '' else hostname
150 0
                hostname_kwds = dict(hostname=hostname)
151 0
                if not "sshport" in kwargs:
152 0
                    sshport = raw_input('Enter sshport [22]: ')
153 0
                    sshport = 22 if sshport == '' else int(sshport)
154 0
                    hostname_kwds['sshport'] = sshport
155 0
                if not 'user' in kwargs:
156 0
                    user = raw_input('Enter username [root]: ')
157 0
                    user = 'root' if user == '' else user
158 0
                    hostname_kwds['user'] = user
159 0
                if not 'password' in kwargs:
160 0
                    password = raw_input('Enter password [root]: ')
161 0
                    password = 'root' if password == '' else password
162 0
                    hostname_kwds['password'] = password
163 0
            self.parameters.update(hostname_kwds)
164

165
        # optional: write configuration back to config file
166 3
        self.c["redpitaya"] = self.parameters
167

168
        # save default port definition for possible automatic port change
169 3
        self.parameters['defaultport'] = self.parameters['port']
170
        # frequency_correction is accessed by child modules
171 3
        self.frequency_correction = self.parameters['frequency_correction']
172
        # memorize whether server is running - nearly obsolete
173 3
        self._serverrunning = False
174 3
        self.client = None  # client class
175 3
        self._slaves = []  # slave interfaces to same redpitaya
176 3
        self.modules = OrderedDict()  # all submodules
177

178
        # provide option to simulate a RedPitaya
179 3
        if self.parameters['hostname'] in ['_FAKE_REDPITAYA_', '_FAKE_']:
180 0
            self.startdummyclient()
181 0
            self.logger.warning("Simulating RedPitaya because (hostname=="
182
                                +self.parameters["hostname"]+"). Incomplete "
183
                                "functionality possible. ")
184 0
            return
185 3
        elif self.parameters['hostname'] in ['_NONE_']:
186 0
            self.modules = []
187 0
            self.logger.warning("No RedPitaya created (hostname=="
188
                                + self.parameters["hostname"] + ")."
189
                                " No hardware modules are available. ")
190 0
            return
191
        # connect to the redpitaya board
192 3
        self.start_ssh()
193
        # start other stuff
194 0
        if self.parameters['reloadfpga']:  # flash fpga
195 0
            self.update_fpga()
196 0
        if self.parameters['reloadserver']:  # reinstall server app
197 0
            self.installserver()
198 0
        if self.parameters['autostart']:  # start client
199 0
            self.start()
200 0
        self.logger.info('Successfully connected to Redpitaya with hostname '
201
                         '%s.'%self.ssh.hostname)
202 0
        self.parent = self
203

204 3
    def start_ssh(self, attempt=0):
205
        """
206
        Extablishes an ssh connection to the RedPitaya board
207

208
        returns True if a successful connection has been established
209
        """
210 3
        try:
211
            # close pre-existing connection if necessary
212 3
            self.end_ssh()
213 3
        except:
214 3
            pass
215 3
        if self.parameters['hostname'] == "_FAKE_REDPITAYA_":
216
            # simulation mode - start without connecting
217 0
            self.logger.warning("(Re-)starting client in dummy mode...")
218 0
            self.startdummyclient()
219 0
            return True
220
        else:  # normal mode - establish ssh connection and
221 3
            try:
222
                # start ssh connection
223 3
                self.ssh = SshShell(hostname=self.parameters['hostname'],
224
                                    sshport=self.parameters['sshport'],
225
                                    user=self.parameters['user'],
226
                                    password=self.parameters['password'],
227
                                    delay=self.parameters['delay'],
228
                                    timeout=self.parameters['timeout'])
229
                # test ssh connection for exceptions
230 0
                self.ssh.ask()
231 3
            except BaseException as e:  # connection problem
232 3
                if attempt < 3:
233
                    # try to connect up to 3 times
234 3
                    return self.start_ssh(attempt=attempt+1)
235
                else:  # even multiple attempts did not work
236 3
                    raise ExpectedPyrplError(
237
                        "\nCould not connect to the Red Pitaya device with "
238
                        "the following parameters: \n\n"
239
                        "\thostname: %s\n"
240
                        "\tssh port: %s\n"
241
                        "\tusername: %s\n"
242
                        "\tpassword: ****\n\n"
243
                        "Please confirm that the device is reachable by typing "
244
                        "its hostname/ip address into a web browser and "
245
                        "checking that a page is displayed. \n\n"
246
                        "Error message: %s" % (self.parameters["hostname"],
247
                                               self.parameters["sshport"],
248
                                               self.parameters["user"],
249
                                               e))
250
            else:
251
                # everything went well, connection is established
252
                # also establish scp connection
253 0
                self.ssh.startscp()
254 0
                return True
255

256 3
    def switch_led(self, gpiopin=0, state=False):
257 0
        self.ssh.ask("echo " + str(gpiopin) + " > /sys/class/gpio/export")
258 0
        sleep(self.parameters['delay'])
259 0
        self.ssh.ask(
260
            "echo out > /sys/class/gpio/gpio" +
261
            str(gpiopin) +
262
            "/direction")
263 0
        sleep(self.parameters['delay'])
264 0
        if state:
265 0
            state = "1"
266
        else:
267 0
            state = "0"
268 0
        self.ssh.ask("echo " + state + " > /sys/class/gpio/gpio" +
269
            str(gpiopin) + "/value")
270 0
        sleep(self.parameters['delay'])
271

272 3
    def update_fpga(self, filename=None):
273 0
        if filename is None:
274 0
            try:
275 0
                source = self.parameters['filename']
276 0
            except KeyError:
277 0
                source = None
278 0
        self.end()
279 0
        sleep(self.parameters['delay'])
280 0
        self.ssh.ask('rw')
281 0
        sleep(self.parameters['delay'])
282 0
        self.ssh.ask('mkdir ' + self.parameters['serverdirname'])
283 0
        sleep(self.parameters['delay'])
284 0
        if source is None or not os.path.isfile(source):
285 0
            if source is not None:
286 0
                self.logger.warning('Desired bitfile "%s" does not exist. Using default file.',
287
                                    source)
288 0
            source = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'fpga', 'red_pitaya.bin')
289 0
        if not os.path.isfile(source):
290 0
            raise IOError("Wrong filename",
291
              "The fpga bitfile was not found at the expected location. Try passing the arguments "
292
              "dirname=\"c://github//pyrpl//pyrpl//\" adapted to your installation directory of pyrpl "
293
              "and filename=\"red_pitaya.bin\"! Current dirname: "
294
              + self.parameters['dirname'] +
295
              " current filename: "+self.parameters['filename'])
296 0
        for i in range(3):
297 0
            try:
298 0
                self.ssh.scp.put(source,
299
                             os.path.join(self.parameters['serverdirname'],
300
                                          self.parameters['serverbinfilename']))
301 0
            except (SCPException, SSHException):
302
                # try again before failing
303 0
                self.start_ssh()
304 0
                sleep(self.parameters['delay'])
305
            else:
306 0
                break
307
        # kill all other servers to prevent reading while fpga is flashed
308 0
        self.end()
309 0
        self.ssh.ask('killall nginx')
310 0
        self.ssh.ask('systemctl stop redpitaya_nginx') # for 0.94 and higher
311 0
        self.ssh.ask('cat '
312
                 + os.path.join(self.parameters['serverdirname'], self.parameters['serverbinfilename'])
313
                 + ' > //dev//xdevcfg')
314 0
        sleep(self.parameters['delay'])
315 0
        self.ssh.ask('rm -f '+ os.path.join(self.parameters['serverdirname'], self.parameters['serverbinfilename']))
316 0
        self.ssh.ask("nginx -p //opt//www//")
317 0
        self.ssh.ask('systemctl start redpitaya_nginx') # for 0.94 and higher #needs test
318 0
        sleep(self.parameters['delay'])
319 0
        self.ssh.ask('ro')
320

321 3
    def fpgarecentlyflashed(self):
322 0
        self.ssh.ask()
323 0
        result =self.ssh.ask("echo $(($(date +%s) - $(date +%s -r \""
324
        + os.path.join(self.parameters['serverdirname'], self.parameters['serverbinfilename']) +"\")))")
325 0
        age = None
326 0
        for line in result.split('\n'):
327 0
            try:
328 0
                age = int(line.strip())
329 0
            except:
330 0
                pass
331
            else:
332 0
                break
333 0
        if not age:
334 0
            self.logger.debug("Could not retrieve bitfile age from: %s",
335
                            result)
336 0
            return False
337 0
        elif age > 10:
338 0
            self.logger.debug("Found expired bitfile. Age: %s", age)
339 0
            return False
340
        else:
341 0
            self.logger.debug("Found recent bitfile. Age: %s", age)
342 0
            return True
343

344 3
    def installserver(self):
345 0
        self.endserver()
346 0
        sleep(self.parameters['delay'])
347 0
        self.ssh.ask('rw')
348 0
        sleep(self.parameters['delay'])
349 0
        self.ssh.ask('mkdir ' + self.parameters['serverdirname'])
350 0
        sleep(self.parameters['delay'])
351 0
        self.ssh.ask("cd " + self.parameters['serverdirname'])
352
        #try both versions
353 0
        for serverfile in ['monitor_server','monitor_server_0.95']:
354 0
            sleep(self.parameters['delay'])
355 0
            try:
356 0
                self.ssh.scp.put(
357
                    os.path.join(os.path.abspath(os.path.dirname(__file__)), 'monitor_server', serverfile),
358
                    self.parameters['serverdirname'] + self.parameters['monitor_server_name'])
359 0
            except (SCPException, SSHException):
360 0
                self.logger.exception("Upload error. Try again after rebooting your RedPitaya..")
361 0
            sleep(self.parameters['delay'])
362 0
            self.ssh.ask('chmod 755 ./'+self.parameters['monitor_server_name'])
363 0
            sleep(self.parameters['delay'])
364 0
            self.ssh.ask('ro')
365 0
            result = self.ssh.ask("./"+self.parameters['monitor_server_name']+" "+ str(self.parameters['port']))
366 0
            sleep(self.parameters['delay'])
367 0
            result += self.ssh.ask()
368 0
            if not "sh" in result: 
369 0
                self.logger.debug("Server application started on port %d",
370
                              self.parameters['port'])
371 0
                return self.parameters['port']
372
            else: # means we tried the wrong binary version. make sure server is not running and try again with next file
373 0
                self.endserver()
374
        
375
        #try once more on a different port
376 0
        if self.parameters['port'] == self.parameters['defaultport']:
377 0
            self.parameters['port'] = random.randint(self.parameters['defaultport'],50000)
378 0
            self.logger.warning("Problems to start the server application. Trying again with a different port number %d",self.parameters['port'])
379 0
            return self.installserver()
380
        
381 0
        self.logger.error("Server application could not be started. Try to recompile monitor_server on your RedPitaya (see manual). ")
382 0
        return None
383
    
384 3
    def startserver(self):
385 0
        self.endserver()
386 0
        sleep(self.parameters['delay'])
387 0
        if self.fpgarecentlyflashed():
388 0
            self.logger.info("FPGA is being flashed. Please wait for 2 "
389
                            "seconds.")
390 0
            sleep(2.0)
391 0
        result = self.ssh.ask(self.parameters['serverdirname']+"/"+self.parameters['monitor_server_name']
392
                          +" "+ str(self.parameters['port']))
393 0
        if not "sh" in result: # sh in result means we tried the wrong binary version
394 0
            self.logger.debug("Server application started on port %d",
395
                              self.parameters['port'])
396 0
            self._serverrunning = True
397 0
            return self.parameters['port']
398
        #something went wrong
399 0
        return self.installserver()
400
    
401 3
    def endserver(self):
402 0
        try:
403 0
            self.ssh.ask('\x03') #exit running server application
404 0
        except:
405 0
            self.logger.exception("Server not responding...")
406 0
        if 'pitaya' in self.ssh.ask():
407 0
            self.logger.debug('>') # formerly 'console ready'
408 0
        sleep(self.parameters['delay'])
409
        # make sure no other monitor_server blocks the port
410 0
        self.ssh.ask('killall ' + self.parameters['monitor_server_name'])
411 0
        self._serverrunning = False
412
        
413 3
    def endclient(self):
414 0
        del self.client
415 0
        self.client = None
416

417 3
    def start(self):
418 0
        if self.parameters['leds_off']:
419 0
            self.switch_led(gpiopin=0, state=False)
420 0
            self.switch_led(gpiopin=7, state=False)
421 0
        self.startserver()
422 0
        sleep(self.parameters['delay'])
423 0
        self.startclient()
424

425 3
    def end(self):
426 0
        self.endserver()
427 0
        self.endclient()
428

429 3
    def end_ssh(self):
430 3
        self.ssh.channel.close()
431

432 3
    def end_all(self):
433 0
        self.end()
434 0
        self.end_ssh()
435

436 3
    def restart(self):
437 0
        self.end()
438 0
        self.start()
439

440 3
    def restartserver(self, port=None):
441
        """restart the server. usually executed when client encounters an error"""
442 0
        if port is not None:
443 0
            if port < 0: #code to try a random port
444 0
                self.parameters['port'] = random.randint(2223,50000)
445
            else:
446 0
                self.parameters['port'] = port
447 0
        return self.startserver()
448

449 3
    def license(self):
450 0
        self.logger.info("""\r\n    pyrpl  Copyright (C) 2014-2017 Leonhard Neuhaus
451
    This program comes with ABSOLUTELY NO WARRANTY; for details read the file
452
    "LICENSE" in the source directory. This is free software, and you are
453
    welcome to redistribute it under certain conditions; read the file
454
    "LICENSE" in the source directory for details.\r\n""")
455

456 3
    def startclient(self):
457 0
        self.client = redpitaya_client.MonitorClient(
458
            self.parameters['hostname'], self.parameters['port'], restartserver=self.restartserver)
459 0
        self.makemodules()
460 0
        self.logger.debug("Client started successfully. ")
461

462 3
    def startdummyclient(self):
463 0
        self.client = redpitaya_client.DummyClient()
464 0
        self.makemodules()
465

466 3
    def makemodule(self, name, cls):
467 0
        module = cls(self, name)
468 0
        setattr(self, name, module)
469 0
        self.modules[name] = module
470

471 3
    def makemodules(self):
472
        """
473
        Automatically generates modules from the list RedPitaya.cls_modules
474
        """
475 0
        names = get_unique_name_list_from_class_list(self.cls_modules)
476 0
        for cls, name in zip(self.cls_modules, names):
477 0
            self.makemodule(name, cls)
478

479 3
    def make_a_slave(self, port=None, monitor_server_name=None, gui=False):
480 0
        if port is None:
481 0
            port = self.parameters['port'] + len(self._slaves)*10 + 1
482 0
        if monitor_server_name is None:
483 0
            monitor_server_name = self.parameters['monitor_server_name'] + str(port)
484 0
        slaveparameters = dict(self.parameters)
485 0
        slaveparameters.update(dict(
486
                         port=port,
487
                         autostart=True,
488
                         reloadfpga=False,
489
                         reloadserver=False,
490
                         monitor_server_name=monitor_server_name,
491
                         silence_env=True))
492 0
        r = RedPitaya(**slaveparameters) #gui=gui)
493 0
        r._master = self
494 0
        self._slaves.append(r)
495 0
        return r

Read our documentation on viewing source code .

Loading