3
\$\begingroup\$

I did this project last year, under some interesting constraints: it's a thin webserver which lives in a Raspberry Pi, which is networked to an embedded system in an industrial sign which is running a proprietary and heavily modified version of *nix. I didn't touch the embedded system in the sign at all.

The whole thing is a kinetic sculpture, installed in a private residence.

Because of some of the project constraints (i.e., we were working in someone's house) I wasn't as aggressive refactoring as I would otherwise have been. There are also no tests. Bad, I know, but it was a situation where the complexity of the project was almost certainly not going to grown in any dramatic way beyond its current level.

Here is the code for the Tornado server. A sanitized fork of the entire repo is here.

The stack is Python/Tornado, HTML, JS and SASS.

""" theSculpture server.

    Launch the server with 'python sculptureController.py' Currently the server defaults to port
    8888. The server provides an interface to the theSculpture,
    allowing the sculpture's mode to be changed.

    Front end and back end code by Liav Koren 2013 - 2014. """

    import tornado.httpserver
    import tornado.ioloop
    import tornado.options
    import tornado.web
    import tornado.auth
    import tornado.escape
    import os
    from tornado.options import define, options
    import tornado.httputil
    import time
    import paramiko
    import subprocess
    import json

    define("port", default=8888, help="run on the given port", type=int)


    class ModeHandler(tornado.web.RequestHandler):
        ''' Handler for the main interface. This serves up the mode selector page. '''

        ssh = ""

        def get(self):
            self.render('modes.html', current_mode='TESTING')

        def post(self):
            """ Mode handler. The mode selector page presents 'slow', 'medium', 'fast',
            'return to landing page' and 'logout' options to user. This handler is also
            the endpoint for the main page options 'ON' and 'OFF'. Slow, medium, fast, on
            and off all initiate a series of remote operations on theSculpture that involve
            opening an SSH tunnel, triggering various shell scripts and transfering files
            between the Pi and theSculpture. """

            path = ""
            data = self.request.arguments

            if 'slow' in data or 'slow-swatch' in data:
                path = '/home/pi/theSculpture/dat/slower1.dat'
            if 'medium' in data or 'medium-swatch' in data:
                path = '/home/pi/theSculpture/dat/med.dat'
            if 'fast' in data  or 'fast-swatch' in data:
                path = '/home/pi/theSculpture/dat/AGH4.dat'
            if 'on' in data or 'on-swatch' in data:
                self.kill_theSculpture()
                self.reset_theSculpture()
                time.sleep(5)
                self.write(json.dumps({"file_transfer": "finished"}))
            if 'off' in data or 'off-swatch' in data:
                self.kill_theSculpture()
                time.sleep(5)
                self.write(json.dumps({"file_transfer": "finished"}))

            if path:
                self.file_transfer(path)
                #self.file_transfer_mock(path)

        def file_transfer_mock(self, path):
            """ A debugging utility method for accepting commands but not
            passing them on to theSculpture. """
            new_path = "new path is: %s" % path
            print new_path
            time.sleep(10)
            self.write(json.dumps({"file_transfer": "finished"}))

        def kill_theSculpture(self):
            """ Turns theSculpture off. """
            cmd = "cd /bin; ./kill_theSculpture.sh"
            self.talk_to_sign(cmd)

        def reset_theSculpture(self):
            """ Restarts theSculpture from an off or currently running state. """
            self.kill_theSculpture()
            self.talk_to_sign("/bin/reset_theSculpture.sh")

        def file_transfer(self, path):
            """ This function deals with resetting theSculpture and
            transferring the new dat files. """
            self.kill_theSculpture()
            print "waiting 30s"
            time.sleep(30)
            print "starting scp"
            proc = subprocess.Popen(['scp', path,
                                    'root@***.**.57.230:/data/theSculpture.dat'],
                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            proc.wait()
            print "stdOut: %s " % proc.stdout.readline()
            print "stdErr: %s" % proc.stderr.readline()
            self.talk_to_sign("/bin/reset_theSculpture.sh")
            self.write(json.dumps({"file_transfer": "finished"}))

        def talk_to_sign(self, cmd):
            ''' Talk_to_sign calls open_ssh to get an ssh client object, then emits
            commands to the target system.'''
            if not self.ssh:
                self.ssh = self.open_ssh_tunnel('***.**.57.230', 'root', 'foobar')
            stdin, stdout, stderr = self.ssh.exec_command(cmd)
            if stderr.readlines():
                print "Error: %s" % stderr.readlines()
            if stdout.readlines():
                print "Cmd returned: %s" % stdout.readlines()


        def get_current_sign_status(self): # ToDo: narrow the exception handling
            ''' Tries to get the current state of the sign and return it to the
            client. '''
            if not self.ssh:
                self.ssh = self.open_ssh_tunnel('***.***.1.105', 'pi', 'foobar')
            stdin, stdout, stderr = self.ssh.exec_command('cat test')
            if stderr.readlines():
                print "Error reading current sign state."
            output = stdout.readlines()
            if output:
                print "current sign state is: "
                print output[0][:-1]
                return output[0][:-1]


        def open_ssh_tunnel(self, ip, username, passwd):
            ''' Provides a ssh client to the talk_to_sign method '''
            try:
                s = paramiko.SSHClient()
                s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                s.connect(ip, username = username, password = passwd)
                return s
            except paramiko.AuthenticationException:
                self.write(json.dumps(
                    {"status": "Failed to login to sign server successfully."}))
                return
            except paramiko.ChannelException:
                self.write(json.dumps(
                    {"status": "Error opening channel to sign server."}))
                return
            except paramiko.SSHException:
                self.write(json.dumps(
                    {"status": "SSH error when trying to contact sign server."}))
                return


    class MainHandler(ModeHandler):
        """ The landing page once the user has logged in. """

        def get(self):
            self.render('main_revised.html')


    def main():
        tornado.options.parse_command_line()
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "static"),
            debug=True,
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            login_url="/login",
        )
        application = tornado.web.Application([
            (r"/mode", ModeHandler),
            (r"/", MainHandler),
            (r"/main", MainHandler),
        ], **settings)
        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(options.port)
        tornado.ioloop.IOLoop.instance().start()

    if __name__ == "__main__":
        main()
\$\endgroup\$
2
  • \$\begingroup\$ I can't comment on the code, but a full Rails stack with Webrick runs fine on RP with no modification. \$\endgroup\$
    – Gene
    Commented May 11, 2014 at 19:09
  • \$\begingroup\$ Hey Gene. It was a pretty light-weight application, I really just needed something that could stand-up some static webpages and handle requests -- anything with an ORM would have been major overkill. \$\endgroup\$
    – LiavK
    Commented May 11, 2014 at 22:00

1 Answer 1

1
\$\begingroup\$

Seems reasonable given your tools at hand and the constraints.

Next time you want to programmatically drive SSH look into http://www.fabfile.org/. It uses paramiko internally and makes all of the complexities much nicer.

\$\endgroup\$
1
  • \$\begingroup\$ We use Fab at our office. I hadn't been introduced to it at the time, but it's really nice. \$\endgroup\$
    – LiavK
    Commented May 13, 2014 at 14:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.