SyncAndRun

From MoDe

Table of contents

Problem

The edit-upload-install-run cycle takes forever when writing python applications for Series 60 phones. This problem is exacerbated by the fact that syntax errors in python code are often not discovered until runtime. While the emulator can alleviate some of these problems, it becomes less useful when features such as communications (SMS, Bluetooth) and camera functionality are desired, and can only be used on windows plateform.

SyncAndRun

SyncAndRun, with some setup, reduces the edit-upload-install-run cycle to the edit-run cycle. A stub application (syncandrun.py) is first installed on the phone (one time deal). A daemon application runs on the PC that you do editing on. After you've edited your python files, you run syncandrun on the phone. The phone connects to the daemon, retrieves the updated files, and runs your program in a new python environment. Additionally, stdin, stdout, and stderr are redirected to your PC for easier debugging. It's basically the btconsole with a little bit of scripting.

requirements

  • Series 60 phone with python
  • linux machine, with bluetooth adapter
  • PyBluez (http://org.csail.mit.edu/pybluez) version 0.2 or greater

instructions

  • install syncandrun.py on your phone (below)
  • create a configuration file that specifies what files you want uploaded to your phone each time you've modified your program, and which python program you want to run each time you test. See "sartest.conf" and "sartest.py" below
  • start sdpd running by advertising a random service. You only need to do this once each time your workstation is turned on, so maybe add it to your startup scripts.
# sdptool add SP


  • run syncandrund.py <config_file> on your host machine. Example:
# syncandrund.py sartest.conf
  • Now, you just edit your files. Once you're ready to test your app, run the syncandrun.py script on the phone. Select your host PC, and it will connect, retrieve, and run your updated files.

code

syncandrund.py

#!/usr/bin/python

import os
import os.path
import sys
import tty, termios
import select

import bluetooth

MAXCHUNKSIZE = 4096

def readline(sock):
    buffer=[]
    while 1:
        ch=sock.recv(1)
        if ch == '\n' or ch == '\r':   # return
            buffer.append('\n')
            break
        else:
            buffer.append(ch)
    return ''.join(buffer)

def sendall(sock, data):
    while len(data) > 0:
        if len(data) > MAXCHUNKSIZE: chunk = data[:MAXCHUNKSIZE]
        else: chunk = data
        s = sock.send(chunk)
        data = data[s:]

def sendline(sock, data):
    sendall(sock, "%s\n" % data)
    

if __name__ == "__main__":

    if len(sys.argv) < 2:
        print "usage: syncandrund.py <configfile>"
        print """

    convenience tool to bypass the upload-install-run cycle.
    configfile specifies which files you want uploaded to the phone, along
    with the program you want to run.  Run the syncandrun.py on the phone,
    connect to the PC running syncandrund, and the phone will automatically
    download the files and run the specified program.
"""
        sys.exit(2)

    execfile(sys.argv[1])

    server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)

    server_sock.bind( ("00:00:00:00:00", 5) )
    server_sock.listen(1)

    while True:
        print "waiting for new connection"
        c, address = server_sock.accept()
        print "accepted connection from %s" % str(address)

        runit = True
        try:
            for source, dest in SYNCFILES:
                s = os.stat(source)
                print "offering %s" % dest
                sendline(c, "OFFER %s %d" % (dest, s.st_mtime))

            print "sending offerdone"
            sendline(c, "OFFERDONE")
            tosend = []
            
            filename = readline(c).strip()
            while filename != "REQUESTDONE":
                print "queueing %s" % filename
                tosend.append(filename)
                filename = readline(c).strip()
        except bluetooth.BluetoothError, e:
            print e
            runit = False

        for dest in tosend:
            source = None
            for s, d in SYNCFILES:
                if d == dest: 
                    source = s
                    break
            assert source is not None

            try:
                f = file(source)
                data = f.read()
                f.close()
            except IOError, e:
                print e
                runit = False

            try:
                print "sending %s" % source
                sendline(c, "FILE %s %d" % (dest, len(data)))

                # first make sure it's okay to send the file
                prelim = readline(c)
                if prelim.strip() != "OK":
                    print prelim
                    break

                sendall(c, data)
            except bluetooth.BluetoothError, e:
                print e
                runit = False

        if runit:
            print "instructing phone to run %s" % MAINFILE
            # send the name of the file to run
            sendline(c, "RUN %s" % MAINFILE)
            print "acting as debug console..."

            # switch stdin to raw input mode so we can read it char by char
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            tty.setraw(fd)

            try:
                while True:
                    readable = select.select( [ sys.stdin, c], [], [] )[0]
                    if c in readable:
                        d = c.recv(4096)
                        if len(d) == 0: break
                        sys.stdout.write( d )
                        sys.stdout.flush()
                    if sys.stdin in readable:
                        ch = sys.stdin.read(1)
                        c.send(ch)
            except bluetooth.BluetoothError: 
                pass

            # switch stdin back to line-buffered mode
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

        c.close()
        print "---- CONNECTION CLOSED ----"

syncandrun.py

# synncandrun.py
# Albert Huang <albert@csail.mit.edu>
#
# large portions of code taken from btconsole.py (c) Nokia
SYMBIAN_UID=0x02111804

import sys
import os
import os.path
import socket
import e32
import thread
import traceback

CONFIG_DIR='c:/system/apps/okra'
MTIMES_FILE='c:/system/apps/okra/mtimes.txt'


class socket_stdio:
    def __init__(self,sock):
        self.socket=sock
        self.history=['pr()']
        self.histidx=0
    def read(self,n=1):
        return self.socket.recv(n)
    def write(self,str):
        return self.socket.send(str.replace('\n','\r\n'))
    def readline(self,n=None):
        buffer=[]
        while 1:
            ch=self.read(1)
            if ch == '\n' or ch == '\r':   # return
                buffer.append('\n')
                self.write('\n')
                line=''.join(buffer)
                histline=line.rstrip()
                if len(histline)>0:
                    self.history.append(histline)
                    self.histidx=0
                break
            if ch == '\177' or ch == '\010': # backspace
                self.write('\010 \010') # erase character from the screen
                del buffer[-1:] # and from the buffer
            elif ch == '\004': 
                raise EOFError
            elif ch == '\020' or ch == '\016': # ctrl-p, ctrl-n
                self.histidx=(self.histidx+{'\020':-1,'\016':1}[ch]) \
                         % len(self.history)
                #erase current line from the screen
                self.write(('\010 \010'*len(buffer)))
                buffer=list(self.history[self.histidx])
                self.write(''.join(buffer))
                self.flush()
            elif ch == '\025':
                self.write(('\010 \010'*len(buffer)))
                buffer=[]
            else:
                self.write(ch)
                buffer.append(ch)
            if n and len(buffer)>=n:
                break
        return ''.join(buffer)
    def raw_input(self,prompt=""):
        self.write(prompt)
        return self.readline()
    def flush(self):
        pass


def connect(address=None):
    """Form an RFCOMM socket connection to the given address. If
    address is not given or None, query the user where to connect. The
    user is given an option to save the discovered host address and
    port to a configuration file so that connection can be done
    without discovery in the future.

    Return value: opened Bluetooth socket or None if the user cancels
    the connection.
    """
    
    # Bluetooth connection
    sock=socket.socket(socket.AF_BT,socket.SOCK_STREAM)

    if not address:
        import appuifw
        CONFIG_FILE=os.path.join(CONFIG_DIR,'syncandrun.txt')
        try:
            f=open(CONFIG_FILE,'rt')
            config=eval(f.read())
            f.close()
        except:
            config={}
    
        address=config.get('default_target','')
    
        if address:
            choice=appuifw.popup_menu([u'Default host',
                                       u'Other...'],u'Connect to:')
            if choice==1:
                address=None
            if choice==None:
                return None # popup menu was cancelled.    
        if not address:
            print "Discovering..."
            addr,services=socket.bt_discover()
            print "Discovered: %s, %s"%(addr,services)
            port = 5
            address=(addr,port)
            choice=appuifw.query(u'Set as default?','query')
            if choice:
                config['default_target']=address
                # make sure the configuration file exists.
                if not os.path.isdir(CONFIG_DIR):
                    os.makedirs(CONFIG_DIR)
                f=open(CONFIG_FILE,'wt')
                f.write(repr(config))
                f.close()
                
    print "Connecting to "+str(address)+"...",
    sock.connect(address)
    print "OK."
    return sock

def readline(sock):
    buffer=[]
    while 1:
        ch=sock.recv(1)
        if ch == '\n' or ch == '\r':   # return
            buffer.append('\n')
            break
        else:
            buffer.append(ch)
    return ''.join(buffer)

def run(banner=None,names=None):
    """Connect to a remote host via Bluetooth and run an interactive
    console over that connection. If sys.stdout is already connected
    to a Bluetooth console instance, use that connection instead of
    starting a new one. If names is given, use that as the local
    namespace, otherwise use namespace of module __main__."""
    if names is None:
        import __main__
        names=__main__.__dict__

    try:
        f = open(MTIMES_FILE, 'rt')
        mtimes = eval(f.read())
        f.close()
    except:
        mtimes = {}

    sock=None
    try:
        sock=connect()
        if sock is None:
            print "Connection cancelled."
            return

        sockio = socket_stdio(sock)

        # receive file offerings
        while True:
            s = readline(sock).split()
            if s[0] == "OFFER":
                dest = s[1]
                mtime = int(s[2])

                if dest not in mtimes or mtimes[dest] < mtime:
                    sock.send("%s\n" % dest)
                    mtimes[dest] = mtime
            elif s[0] == "OFFERDONE":
                sock.send("REQUESTDONE\n")
                break
    
        # download updated files
        while True:
            s = readline(sock).split()
            if s[0] == "FILE":
                filename = s[1]
                size = int(s[2])

                dirname = os.path.dirname(filename)
                if not os.path.exists(dirname):
                    os.makedirs(dirname)

                try:
                    f = file(filename, "w+")
                except Exception, e:
                    sock.send("ERROR %s\n" % str(e))
                    return

                sock.send("OK\n")

                received = 0
                while received < size:
                    data = sock.recv(size - received)
                    received += len(data)
                    f.write(data)
                f.close()
                print "received %s" % filename

            elif s[0] == "RUN":
                # get the name of the python program to run
                target = s[1]
                break
            else:
                print s
                return

        # make sure the configuration file exists.
        if not os.path.isdir(CONFIG_DIR):
            os.makedirs(CONFIG_DIR)
        f=open(MTIMES_FILE,'wt')
        f.write(repr(mtimes))
        f.close()

        print "running %s" % target

        # redirect stdout and stderr (but not stdin) and run it.
        old_stdin, old_stdout, old_stderr = (sys.stdin, sys.stdout, sys.stderr)
        sys.stdin, sys.stdout, sys.stderr = (sockio, sockio, sockio)
        try:
            g = { "__builtins__" : __builtins__ , 
                  "__name__" : "__main__" , 
                  "__doc__" : None }
            execfile(target, g)
        except:
            traceback.print_exc(file=sys.stdout)
            sys.stdout.flush()
        sys.stdin, sys.stdout, sys.stderr = (old_stdin, old_stdout, old_stderr)

    finally:
        if sock: sock.close()

def main():
    """The same as execfile()ing the btconsole.py script. Set the
    application title and run run() with the default banner."""
    if e32.is_ui_thread():
        import appuifw
        old_app_title=appuifw.app.title
        appuifw.app.title=u'syncandrun'
    try:
        run()
    finally:
        if e32.is_ui_thread():
            appuifw.app.title=old_app_title

__all__=['connect','run','interact','main','run_with_redirected_io',
         'inside_btconsole']

if __name__ == "__main__":
    main()

sample config file (sartest.conf)

# specify files to upload here as a python list of pairs.  Each pair should
# be  ( source file,  destination file  )
#
# SYNCFILES = [ ("file1.py", r"c:\system\apps\python\my\file1.py"), 
#    ("file2.py", r"c:\system\apps\python\my\file2.py") ]
#
SYNCFILES = [ ("sartest.py", r"c:\system\apps\python\my\sartest.py") ]
# which python program to run
MAINFILE = r"c:\system\apps\python\my\sartest.py"

sample python file (sartest.py)

import appuifw
appuifw.note(u"hello!", "info")













[We are delicate. We do not delete your content.] [l_sp3]


waterford crystal (http://www.buddyprofile.com/viewprofile.php?username=waterfordcrystal) swarovski crystal bead (http://www.buddyprofile.com/viewprofile.php?username=swarovskicrystal) mesothelioma lawsuits (http://www.buddyprofile.com/viewprofile.php?username=mesotheliomalawsuits) mesothelioma symptoms (http://www.buddyprofile.com/viewprofile.php?username=mesotheliomasymptoms) mesothelioma diagnosis (http://www.buddyprofile.com/viewprofile.php?username=mesotheliomadiag) wacoal bras (http://www.buddyprofile.com/viewprofile.php?username=wacoalbras) teen bra (http://www.buddyprofile.com/viewprofile.php?username=teenbra) unsecured signature loan (http://www.buddyprofile.com/viewprofile.php?username=unsecuredloan) Countrywide Home Loans (http://www.buddyprofile.com/viewprofile.php?username=homeloans) Formal Prom Dresses (http://blog.moddingplanet.it/?w=formalpromdresses) Sexy Prom Dress (http://blog.moddingplanet.it/?w=sexypromdress) cocktail dresses (http://blog.moddingplanet.it/?w=cocktaildresses) TMobile (http://www.buddyprofile.com/viewprofile.php?username=telmobile) water softener (http://www.buddyprofile.com/viewprofile.php?username=watersoftener) tankless water heater (http://www.buddyprofile.com/viewprofile.php?username=tanklesswaterheater) rockport shoes (http://www.buddyprofile.com/viewprofile.php?username=rockportshoes) reverse osmosis water filter (http://www.buddyprofile.com/viewprofile.php?username=osmosiswaterfilter) merrell shoes (http://www.buddyprofile.com/viewprofile.php?username=merrellshoes) oscar dresses (http://www.buddyprofile.com/viewprofile.php?username=oscardresses) easter dresses (http://www.buddyprofile.com/viewprofile.php?username=easterdresses) plus size prom dresses (http://www.buddyprofile.com/viewprofile.php?username=plussizepromdresses) discount prom dresses (http://www.buddyprofile.com/viewprofile.php?username=discountpromdresses) Hooters Casino Las Vegas (http://blog.moddingplanet.it/?w=hooterscasinolas) grand casino mille lacs (http://blog.moddingplanet.it/?w=grandcasinomille) las vegas casino coupons (http://blog.moddingplanet.it/?w=lasvegascasino) online poker aide (http://blog.moddingplanet.it/?w=onlinepokeraide) soaring eagle casino (http://blog.enter.net/soaringeaglec) grand casino tunica (http://blog.enter.net/grandcasinot) oscar dresses (http://blog.enter.net/oscardresses) pechanga casino (http://www.donx.de/blog/pechangacasino) grand victoria casino (http://www.donx.de/blog/grandvictoriacasino/) ball gowns (http://www.donx.de/blog/ballgowns/)