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")
