# PyNetMony Netmonitor for S60 3rd Edition phones # -*- coding:latin1 -*- # # Copyright (C) 2007 Carsten Knuetter, Georg Lukas # using positioning because cant get lr installed anymore #nl@mnet-online.de 20131009 # # email: netmonitor.s80@o2online.de # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3 of the License, or (at your # option) any later version. This program is distributed in the hope that # it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. You should have received a # copy of the GNU General Public License along with this program; if not, # see . import appuifw import e32 import location import sysinfo import time import positioning import sys import audio import thread import camera import key_codes from audio import * from graphics import * try: import locationrequestor locreq = True gpsdata = [0, 'GPS disabled'] except: gpsdata = [0, 'Module locationrequestor not installed'] locreq = False SIS_VERSION = '0.3.9' TITLE=u'PyNetMony' VERSION=TITLE + ' ' + SIS_VERSION # temp variables running=True light=True tab=0 draw_rnc = False s = None finder = False # XXX: use absolute path for mp3 etc. # os.path.join(os.getcwd(), u'default_settings.xml') # program config configfile = "c:/system/apps/pynetmony.cfg" defconfig = { 'CellNotify': 0, 'Light': 0, 'Refresh': 0.5, 'Volume': 5, 'Voice': 0, 'GPS': 0 } config = defconfig.copy() # TODO: move these to theme[xxx] font=u"normal" color=(255,255,255) linecol=(255,128,128) #border=(255,0,0) bg=(0,0,60) border=bg headcol=(255,255,255) headbg=(255,255,255) # table for RX level history rxl_log = [] rxl_last = 0 # table for Cell history loc_log = [] logger = None logname = None toggle = 2 usinggps = 0 gpson = 0 # XXX workaround for E90 "losing" link during data traffic offlinetimeout = 90 # global GSM state variables t_last = 0 rxl = None gsmloc = gsmloc_real = None # Spelling for network providers class Voice: offline = u"Offline" band = [u"G S M", u"U M T S"] provider = { 262: { 1: "Timo Beil", 2: "Wo da phon", 3: "E plus", 7: "Oh tuh"} } class Logger: def __init__(self, log_name): self.logfile = log_name def write(self, obj): try: log_file = open(self.logfile, 'a') log_file.write(obj) log_file.close() except IOError, err: appuifw.note(u"Logger: " + unicode(err), "error") def writelines(self, obj): self.write(''.join(list)) def logtab(self, list): # Convert list to unicode and add tabs txt = '\t'.join(map(unicode, list)) + '\n' self.write(txt) def flush(self): pass class Setup( object ): # Config constants SOUND_OFF = 0 SOUND_SOUND = 1 SOUND_VOICE = 2 LIGHT_OFF = 0 LIGHT_CELL = 1 LIGHT_ALWAYS = 2 VOICE_CELL = 0 VOICE_BAND = 1 VOICE_BAND_LAC = 2 VOICE_BAND_LAC_NET = 3 GPS_INTERNAL = 0 GPS_EXTERNAL = 1 ## The constructor. def __init__( self ): global config ## Config options self._iSound = [u'OFF', u'Sound', u'Voice'] self._iLight = [u'OFF', u'Cell change on', u'Always on'] self._iVoice = [u'Cell only', u'Band & Cell', u'Band & Cell & LAC', u'Cell & LAC & NET'] self._iGPS = [u'Internal', u'External'] ## Setup Menu self._iMenu = [(u'Defaults', self.defaultConfig), (u'Reload Config File', self.loadConfig)] ## Form fields self.configToForm() #flags = appuifw.FFormEditModeOnly flags = appuifw.FFormEditModeOnly + appuifw.FFormDoubleSpaced self._iForm = appuifw.Form(self._iFields, flags) self._iForm.save_hook = self.saveForm #self._iForm.flags = appuifw.FFormEditModeOnly self._iForm.menu = self._iMenu def configToForm(self): global config _iFields = [( u'CellNotify', 'combo', ( self._iSound, config['CellNotify'] ) ), ( u'Light', 'combo', ( self._iLight, config['Light'] ) ), ( u'Refresh','float', config['Refresh'] ), ( u'Volume','number', config['Volume'] ), ( u'VoiceText','combo', (self._iVoice, config['Voice']) ), ( u'GPS', 'combo', ( self._iGPS, config['GPS'] ) )] ## XXX: live update doesn't work yet. Need fixing if hasattr(self, '_iFields'): for i in range(len(_iFields)): self._iFields[i] = _iFields[i] else: self._iFields = _iFields def formToConfig(self): global config ## XXX: must find elegant way for conversion Form <-> config config = { 'CellNotify': self._iFields[0][2][1], 'Light': self._iFields[1][2][1], 'Refresh': self._iFields[2][2], 'Volume': self._iFields[3][2], 'Voice': self._iFields[4][2][1], 'GPS': self._iFields[5][2][1] } cfgfile = open(configfile, 'wt') cfgfile.write(repr(config)) cfgfile.close() def defaultConfig(self): global config, defconfig config = defconfig.copy() self.configToForm() appuifw.note(u"Please reopen the Setup dialog.") def loadConfig(self, silent=False): global config config = defconfig.copy() try: cfgfile = open(configfile, 'rt') content = cfgfile.read() cfgfile.close() config.update(eval(content)) if not silent: appuifw.note(u"Please reopen the Setup dialog.") except IOError, error: if not silent: appuifw.note(unicode(error),'error') self.configToForm() ## Displays the form. def execute( self ): self._iForm.execute( ) ## save_hook send True if the form has been saved. def saveForm( self, aConfig ): global config self._iFields = aConfig self.formToConfig() class CellCam: SIZE = (640,480) def __init__(self): global finder self.timeout = 100 self.lock = e32.Ao_lock() finder = True self.oldexit = appuifw.app.exit_key_handler appuifw.app.exit_key_handler = self.abort appuifw.app.body.bind(key_codes.EKeySelect, self.take_picture) camera.start_finder(self.finder_cb) self.lock.wait() def abort(self): camera.stop_finder() self.finish() def finish(self): global finder appuifw.app.body.bind(key_codes.EKeySelect, None) appuifw.app.exit_key_handler = self.oldexit self.lock.signal() finder = False def take_picture(self): global finder, gsmloc fname = rxl_line(gsmloc, fname=True) camera.stop_finder() img = camera.take_photo(size=CellCam.SIZE) w, h = appuifw.app.body.size appuifw.app.body.blit(img,target=(0, 0, w, 0.75 * w), scale = 1) appuifw.app.body.text((10,20), u"Writing Image:", fill=color) appuifw.app.body.text((10,40), fname, fill=color) img.save("E:\\" + fname) self.finish() def finder_cb(self, im): global gsmloc info = rxl_line(gsmloc) im.text((10,20), info, fill=color) #im.text((10,40), u"Feature test: " + unicode(self.timeout), fill=color) appuifw.app.body.blit(im) img=Image.new((800,352)) img_dbl=Image.new((800,352)) def mainmenu_setup(): logmenu = [(u"Start Logging", startlog), (u"Stop Logging", stoplog)] gpsmenu = [(u"Start GPS", startgps), (u"Stop GPS", stopgps)] appuifw.app.menu = [logmenu[logger != None], gpsmenu[gpson], (u"Orientation", rotate), (u"Site Photo...", cellphoto), (u"Settings...", setup)] def menus_setup(): global tab mainmenu_setup() appuifw.app.set_tabs([u"NetMon", u"Graph", u"History", u"GPS", u"About"], set_tab) appuifw.app.activate_tab(tab) def rotate(): if appuifw.app.orientation == 'landscape': appuifw.app.orientation = 'portrait' else: appuifw.app.orientation = 'landscape' def setup(): # Bug in Form(): manually hide Tabs appuifw.app.set_tabs([], None) SetupForm.execute( ) menus_setup() def set_tab(newtab): global tab, gui tab = newtab gui.signal() def startgps(): global gpson, locreq, gpsdata if not locreq: appuifw.note(u"Module Locationrequestor not installed!",'info') #return if not gpson: gpsdata = [0, 'Starting GPS...'] gpson = 1 mainmenu_setup() #thread.start_new_thread(gps_worker,()) gps_worker() def stopgps(): global gpson, gpsdata if gpson: gpsdata =[ 0, 'Stopping GPS...'] gpson = 0 mainmenu_setup() positioning.stop_position() def cellphoto(): appuifw.app.set_tabs([], None) c = CellCam() menus_setup() def startlog(): global logger, logname logname = "e:/NetMonLog_"+time.strftime("%Y%m%d_%H%M%S")+".txt" logger = Logger(logname) #sys.stderr = sys.stdout = my_log logger.logtab(("Date (Y/M/D)", "Time (H:M:S)", "CID", "LCID", "LAC", "MCC", "MNC", "RNC", "RXL", "LON", "LAT")) mainmenu_setup() def stoplog(): global logger logger = None mainmenu_setup() def writelog(list): #my_log = Logger(logname) #sys.stderr = sys.stdout = my_log logger.logtab(list) def sound_play(): global s if not s: return try: s.set_volume(config['Volume']) s.play() except Exception, error: appuifw.note(u"Sound Error:" + unicode(error),'error') def decode_cid(mcc, mnc, cid): global draw_rnc #if (mnc == 7) and (mcc == 262) and (cid > 65535): if cid > 65535: cidhex="%X" % cid rnc=int(cidhex[:-4],16) cid=int(cidhex[-4:],16) draw_rnc = True return (cid, rnc) else: return (cid, 'n/a') def log_rxl(t, rxl): global rxl_log, rxl_last if not rxl: rxl = 0 if rxl_last == 0: rxl_last = t - 1 while t > rxl_last: # rescue last 1000 entries, negate for easier drawing rxl_log = rxl_log[-999:] + [-rxl] rxl_last += 1 def dbm(rxl): if not rxl: return u"Offline" else: return unicode(rxl) + u" dBm" def log_loc(t, gsmloc): global loc_log if len(loc_log) > 0 and gsmloc == loc_log[-1:][0][1]: return loc_log += [[t, gsmloc]] def draw_about(): global img size,offset = appuifw.app.layout(appuifw.EMainPane) img.rectangle((0,0)+size,outline=border,fill=bg) img.text((50,20), VERSION,fill=color) img.text((75,40), u'(c) 2007 by',fill=color) img.text((10,60), u'Carsten Knütter, Georg Lukas',fill=color) img.text((10,90), u'netmonitor.s80@o2online.de',fill=color) img.text((5,110), u'This program comes with',fill=color) img.text((5,130), u'ABSOLUTELY NO WARRANTY',fill=color) img.text((5,160), u'This is free software, and you',fill=color) img.text((5,180), u'are welcome to redistribute',fill=color) img.text((5,200), u'it under certain conditions',fill=color) #img.text((2,220), u'RES: ',fill=(255,255,255), font=(None,40)) #img.text((2,210), u'certain conditions',fill=color) #img.text((35,230), u'Press Options to start... ',fill=color) def draw_netmon(lac, mcc, mnc, rnc, rxl, ver, imei, longcid, cidhex, lachex, longcidhex): try: bars = sysinfo.signal_bars() except SystemError: bars = u"n/a" batty = sysinfo.battery() ram = sysinfo.free_ram()/1024 totram = sysinfo.total_ram()/1024 x = appuifw.app.layout(appuifw.EScreen) if longcid == u'n/a': cidtext = u'CID: ('+unicode(cidhex)+u')' band = u' (GSM)'; else: cidtext = u'LCID: '+unicode(longcid)+u' ('+unicode(longcidhex)+u')' band = u' (UMTS)'; img.text((3,40), cidtext,fill=color) img.text((3,60), u'LAC: '+unicode(lac)+u' ('+unicode(lachex)+u')',fill=color) img.text((3,80), u'RNC: '+unicode(rnc),fill=color) img.text((3,100), u'NET: '+unicode(mcc)+u" - "+unicode(mnc)+band,fill=color) img.text((3,120), u'RXL: '+dbm(rxl)+u" ("+unicode(bars)+u")",fill=color) img.text((3,140), u'BAT: '+unicode(batty)+u" %",fill=color) img.text((3,160), u'RAM: '+unicode(ram)+u" / "+unicode(totram)+u" KB",fill=color) img.text((3,180), u'VER: '+unicode(ver),fill=color) img.text((3,200), u'IMEI: '+imei,fill=color) img.text((3,220), u'RES: '+unicode(x),fill=color) #img.text((3,220), u'APP: '+appuifw.app.full_name(),fill=color) def draw_rxl_line(size, pos, text): sz = (size[1]-2, 20) i = Image.new(sz, '1') i.rectangle((0,0)+sz, fill=0x000000) i.text((2,11), text, 0xffffff) irot = i.transpose(ROTATE_90) img.blit(irot, target=pos, mask=irot) #img.text((0,200), unicode(sz)+unicode(pos)+text, color) def headline(size, cid): (w, h) = size # CLK and CID always on top img.rectangle((0,0,w,20),headcol,fill=headbg) img.text((3,17), u'CID: '+unicode(cid),fill=bg) img.text((w-73,17), unicode(time.strftime("%H:%M:%S")),fill=bg) if logger: img.text((w/2-25,17), u'L',fill=bg) if (gpson == 1): if (len(gpsdata) == 15): gpsstat = unicode(gpsdata[14]) else: gpsstat = u"*" img.text((w/2+5,17), u'G'+gpsstat,fill=bg) def rxl_line(log, fname=False): (mcc, mnc, lac, cid) = log longcid = cid (cid, rnc) = decode_cid(mcc, mnc, cid) if mcc is None or mcc == "n/a": return u"Offline" else: if fname: return unicode(mcc)+"-"+unicode(mnc)+u"_"+unicode(lac)+u"-"+unicode(longcid)+".jpg" else: return unicode(mcc)+"-"+unicode(mnc)+u" C:"+unicode(cid)+u" L:"+unicode(lac) def draw_rxlgraph(size): global rxl_log, loc_log if len(rxl_log) == 0: return t = time.time() w = size[0] h = size[1] sublog = rxl_log[-w:] xcoord = range(w-len(sublog)+1, w+1) line = zip(xcoord, sublog) img.line(line, outline=linecol) x = w l = len(loc_log)-1 #img.text((0,h-36),unicode(loc_log),linecol) while x > -20 and l >= 0: offset = t - loc_log[l][0] x = w - offset img.line((x,0,x,h),outline=linecol) draw_rxl_line(size, (x+1, 1), rxl_line(loc_log[l][1])) l-=1 img.text((3, 40), dbm(-rxl_log[-1:][0]), color) def draw_history_line(x, y, tstamp, net, lac, cid, rnc): global draw_rnc img.text((5+x, y), unicode(tstamp), color) img.text((85+x, y), unicode(cid), color) img.text((140+x, y), unicode(lac), color) if draw_rnc: img.text((200+x, y), unicode(rnc), color) img.text((250+x, y), unicode(net), color) else: img.text((200+x, y), unicode(net), color) def draw_history(size): global loc_log y = 64 count = (size[1]-y)/16 if size[0] > 320: count = 2 * count draw_history_line(400, y-16, "Time", "Net", "LAC", "CID", "RNC") #img.text((0, 10), unicode(count), color) draw_history_line(0, y-16, "Time", "Net", "LAC", "CID", "RNC") x = 0 i = 0 for v in loc_log[-count:]: t = time.strftime("%H:%M:%S ", time.localtime(v[0])) (mcc, mnc, lac, cid) = v[1] if mcc is None: cid = u"Offline" net = lac = rnc = u"" else: (cid, rnc) = decode_cid(mcc, mnc, cid) net = unicode(mcc)+"-"+unicode(mnc) if (i == (count/2)) and (size[0] > 320): x = 400 y = 64 draw_history_line(x, y, t, net, lac, cid, rnc) y += 16 i += 1 def draw_gps(size): global gpsdata, usinggps, gpson (w, h) = size if (gpson == 1) and (len(gpsdata) == 15): img.text((10,40), u'Modul: '+gpsdata[6]+u' ('+unicode(usinggps)+u')',fill=color) img.text((10,60), u'Lat: '+unicode("%9.6f"%gpsdata[1]),fill=color) img.text((10,80), u'Lon: '+unicode("%9.6f"%gpsdata[2]),fill=color) img.text((10,100), u'Altitude: '+unicode("%7.1f"%gpsdata[3])+u' m',fill=color) img.text((10,120), u'Accuracy: '+u'H: '+unicode("%5.1f"%gpsdata[4])+u'm'+u' V: '+unicode("%5.1f"%gpsdata[5])+u'm',fill=color) img.text((10,140), u'Speed: '+unicode("%5.1f"%gpsdata[8])+u' km/h',fill=color) img.text((10,160), u'Heading: '+unicode("%5.1f"%gpsdata[10])+u'°',fill=color) img.text((10,180), u'SAT (Fix): '+unicode(gpsdata[13])+u' ('+unicode(gpsdata[14])+u')',fill=color) elif len(gpsdata) >= 2: # Display Error from gpsdata[1] img.text((10,40), u'No data available:',fill=color) img.text((10,60), unicode(gpsdata[1]),fill=color) else: img.text((int(w/2)-30,100), u'GPS OFF',fill=color) def reader(text): try: audio.say(text) except Exception, error: # no GUI from sub-thread! #appuifw.note(u"Sound Error: "+unicode(error),'error') pass def gps_update(kw): global gpsdata, gpson _pos,_course,_sats = kw['position'], kw['course'] or {}, kw['satellites'] or {} gpsdata = [ 1, _pos['latitude'], #1 _pos['longitude'], #2 _pos['altitude'], #3 _pos['horizontal_accuracy'], #4 _pos['vertical_accuracy'], #5 u'default', #6 Modul positioning.module_info( positioning.default_module() )['name'] None, _course.get('speed',''), #8 None, _course.get('heading',''), #10 None, None, _sats.get('satellites',''), #13 ?? _sats.get('used_satellites',''), #14 ?? ] if (not gpson): #lockgps.signal() gpsdata = [0, 'GPS stopped'] def gps_find_device(lr, type): for m in positioning.modules(): #list of dict(name,id,available flag ) if not m['available']: continue return m['id'] return -1 def gps_worker(): global usinggps # find internal GPS (if any) #if config['GPS'] == Setup.GPS_INTERNAL: usinggps = positioning.default_module() #@@assert available if usinggps < 0: gpsdata = [0, 'Neither internal nor external GPS available'] return positioning.select_module( usinggps ) # install callback try: positioning.set_requestors([ {"type":"service","format":"application","data":"test_app"} ] ) positioning.position( course=1, satellites=1, callback=gps_update,interval=1500000, partial=0) except Exception, reason: appuifw.note(unicode(reason),'error') pass def log_worker(): global gsmloc, gsmloc_real, rxl, gui, t_last, running t_old = 0 while running: t_last = time.time() try: rxl = -sysinfo.signal_dbm() if rxl == 0: # Workaround Nokia dumbness: rxl = None except SystemError: rxl = None try: oldloc = gsmloc gsmloc_real = location.gsm_location() #if t_last % 10 < 3: # gsmloc_real = None # Workaround for "no location during data traffic" if gsmloc_real is None and rxl: # and t_old + offlinetimeout < t_last: # We are busy, simulate last cell for history gsmloc = oldloc else: # Update state gsmloc = gsmloc_real t_old = t_last except (ValueError, SystemError): gsmloc = gsmloc_real = None log_rxl(t_last, rxl) if gsmloc is None or not rxl: log_loc(t_last, (None, None, None, None)) else: log_loc(t_last, gsmloc) gui.signal() e32.ao_sleep(1) def netmonitor(): global running, toggle, rxl, gsmloc, img, img_dbl busytext = ['n/a', 'busy'] imei=sysinfo.imei() ver = sysinfo.os_version() oldcid = 0 oldlac = 0 t_log = 0 while running: # Block when Camera is active while finder: e32.ao_sleep(0.5) size,offset = appuifw.app.layout(appuifw.EMainPane) img.rectangle((0,0)+size,outline=border,fill=bg) if gsmloc_real is None and gsmloc is not None: busy = True else: busy = False if gsmloc is None or not rxl or busy: mcc = mnc = lac = lachex = cid = cidhex = longcid = longcidhex = rnc = cid = busytext[busy] else: (mcc, mnc, lac, cid) = gsmloc if cid > 65535: umts = 1 longcid = cid longcidhex = "%X"%(longcid) else: umts = 0 longcid = longcidhex = busytext[busy] (cid, rnc) = decode_cid(mcc, mnc, cid) try: lachex = "%X"%(lac) except: lachex = busytext[busy] try: cidhex = "%X"%(cid) except: cidhex = busytext[busy] if cid != oldcid: oldcid = cid # Cell changed, notify the user if config['Light']==Setup.LIGHT_CELL: e32.reset_inactivity() if config['CellNotify']==Setup.SOUND_SOUND: sound_play() elif config['CellNotify']==Setup.SOUND_VOICE: #sound.set_volume(config['Volume']) voice = u'' if not rxl: voice = Voice.offline elif config['Voice']==Setup.VOICE_CELL: voice = unicode(cid) elif config['Voice']==Setup.VOICE_BAND: voice = Voice.band[umts] + u" " + unicode(cid) elif config['Voice']==Setup.VOICE_BAND_LAC: voice = Voice.band[umts] + u" " if oldlac != lac: voice += unicode(cid)+u'. L A C '+unicode(lac) else: voice += unicode(cid) elif config['Voice']==Setup.VOICE_BAND_LAC_NET: voice = Voice.band[umts] + u" " if oldlac != lac: if mcc in Voice.provider and mnc in Voice.provider[mcc]: voice += Voice.provider[mcc][mnc] else: voice += u'M C C '+unicode(mcc)+u' M N C '+unicode(mnc) voice += u". " + unicode(cid) + u'. L A C '+unicode(lac) else: voice += unicode(cid) thread.start_new_thread(reader,(voice,)) oldlac = lac if tab==0: draw_netmon(lac, mcc, mnc, rnc, rxl, ver, imei, longcid, cidhex, lachex, longcidhex) elif tab==1: draw_rxlgraph(size) elif tab==2: draw_history(size) elif tab==3: draw_gps(size) elif tab==4: draw_about() # Headline for all but "About" if tab <= 3: headline(size, cid) tmp = img_dbl img_dbl = img img = tmp handle_redraw(()) if logger: if t_log != t_last: # Only log coords when GPS is there and values are not NaN if (gpson == 1) and (len(gpsdata) == 15) and str(gpsdata[1]) != 'NaN': # Convert lat, lon to formatted strings (lon, lat) = map(lambda x: "%2.6f" % x, gpsdata[1:3]) else: lon = lat = 'n/a' if rxl: trxl = rxl else: trxl = 'n/a' writelog((time.strftime("%Y/%m/%d"), time.strftime("%H:%M:%S"), cid, longcid, lac, mcc, mnc, rnc, trxl, lon, lat)) t_log = t_last if config['Light']==Setup.LIGHT_ALWAYS: e32.reset_inactivity() #e32.ao_yield() gui.wait() def handle_redraw(rect): global img_dbl, canvas canvas.blit(img_dbl) def standalone(): # XXX: use appuifw.app.uid() # TITLE in full_name() means standalone return appuifw.app.full_name().find(TITLE) != -1 def exit_key_handler(): if appuifw.query(u'Do you really want to exit?', 'query'): finish() def finish(): global script_lock, running, s script_lock.signal() gui.signal() #appuifw.app.set_exit() running = False if s != None: s.close() appuifw.app.set_tabs([], None) if standalone(): appuifw.app.set_exit() def main(): global script_lock, gui, s, SetupForm, canvas canvas = appuifw.Canvas(event_callback=None, redraw_callback=handle_redraw) #appuifw.app.screen='large' appuifw.app.screen='normal' appuifw.app.title = TITLE appuifw.app.body=canvas script_lock = e32.Ao_lock() gui = e32.Ao_lock() thread.start_new_thread(log_worker,()) SetupForm = Setup( ) SetupForm.loadConfig(silent=True) menus_setup() appuifw.app.exit_key_handler = exit_key_handler appuifw.app.body.bind(key_codes.EKeyNo, exit_key_handler) #if locreq and config['GPS']: # startgps() try: file = u"e:\\fanfare3.mp3" s = Sound.open(file) except Exception, error: appuifw.note(u"Sound Error: "+unicode(error),'error') #audio.say(u'test 123456789') netmonitor() script_lock.wait() if standalone(): # Standalone: Display exception as note try: main() except Exception, error: appuifw.note(u"Fatal Error: "+unicode(error),'error') else: # Python Script Shell: log exception to stdout main()