Initial ussd-common integration
authorkibergus <kibergus@gmail.com>
Tue, 8 Jun 2010 21:05:20 +0000 (21:05 +0000)
committerkibergus <kibergus@gmail.com>
Tue, 8 Jun 2010 21:05:20 +0000 (21:05 +0000)
git-svn-id: file:///svnroot/ussd-widget/trunk@34 d197f4d6-dc93-42ad-8354-0da1f58e353f

ussd4all/debian/rules
ussd4all/src/src.pro
ussd4all/ussdquery/gsmdecode.py [new file with mode: 0644]
ussd4all/ussdquery/ussdquery.py [new file with mode: 0755]

index a07efb6..f58f7bb 100755 (executable)
@@ -29,6 +29,13 @@ install: build
 
        # Add here commands to install the package into debian/your_appname
        cd builddir && $(MAKE) INSTALL_ROOT=$(CURDIR)/debian/$(APPNAME) install
+
+       mkdir -p "$(CURDIR)/debian/ussd-common"
+       mkdir -p "$(CURDIR)/debian/ussd-common/usr/lib/python2.5/"
+       cp -a "ussdquery/gsmdecode.py" "$(CURDIR)/debian/ussd-common/usr/lib/python2.5/gsmdecode.py"
+       mkdir -p "$(CURDIR)/debian/ussd-common/usr/bin/"
+       cp -a "ussdquery/ussdquery.py" "$(CURDIR)/debian/ussd-common/usr/bin/ussdquery.py"
+
 # Build architecture-independent files here.
 binary-indep: build install
 # We have nothing to do by default.
index 5d2df32..7007ec7 100644 (file)
@@ -4,10 +4,6 @@ TARGET = qussd
 
 QT += maemo5
 
-# install
-target.path = $$[QT_INSTALL_EXAMPLES]/opt/maemo/usr/bin/qussd
-INSTALLS += target
-
   unix {
     #VARIABLES
     isEmpty(PREFIX) {
diff --git a/ussd4all/ussdquery/gsmdecode.py b/ussd4all/ussdquery/gsmdecode.py
new file mode 100644 (file)
index 0000000..5a8d3e3
--- /dev/null
@@ -0,0 +1,305 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+## 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; version 2 and higer.
+##
+## Martin Grimme (martin.grimme # gmail.com) 2010
+## Guseynov Alexey (kibergus # gmail.com) 2010
+
+LANG_DE = 0x0
+LANG_EN = 0x1
+LANG_IT = 0x2
+LANG_FR = 0x3
+LANG_ES = 0x4
+LANG_NL = 0x5
+LANG_SE = 0x6
+LANG_DA = 0x7
+LANG_PO = 0x8
+LANG_FI = 0x9
+LANG_NO = 0xa
+LANG_GR = 0xb
+LANG_TR = 0xc
+LANG_UNSPECIFIED = 0xf
+
+
+GSM_DEFAULT_ALPHABET = [
+    u"@",
+    u"\u00a3",
+    u"$",
+    u"\u00a5",
+    u"\u00e8",
+    u"\u00e9",
+    u"\u00f9",
+    u"\u00ec",
+    u"\u00f2",
+    u"\u00c7",
+    u"\n",
+    u"\u00d8",
+    u"\u00f8",
+    u"\r",
+    u"\u00c5",
+    u"\u00e5",
+    
+    u"\u0394",
+    u"_",
+    u"\u03a6",
+    u"\u0393",
+    u"\u039b",
+    u"\u03a9",
+    u"\u03a0",
+    u"\u03a8",
+    u"\u03a3",
+    u"\u0398",
+    u"\u039e",
+    u" ",
+    u"\u00c6",
+    u"\u00e6",
+    u"\u00df",
+    u"\u00c9",
+    
+    u" ",
+    u"!",
+    u"\"",
+    u"#",
+    u"\u00a4",
+    u"%",
+    u"&",
+    u"'",
+    u"(",
+    u")",
+    u"*",
+    u"+",
+    u",",
+    u"-",
+    u".",
+    u"/",
+    
+    u"0",
+    u"1",
+    u"2",
+    u"3",
+    u"4",
+    u"5",
+    u"6",
+    u"7",
+    u"8",
+    u"9",
+    u":",
+    u";",
+    u"<",
+    u"=",
+    u">",
+    u"?",
+    
+    u"\u00a1",
+    u"A",
+    u"B",
+    u"C",
+    u"D",
+    u"E",
+    u"F",
+    u"G",
+    u"H",
+    u"I",
+    u"J",
+    u"K",
+    u"L",
+    u"M",
+    u"N",
+    u"O",
+    
+    u"P",
+    u"Q",
+    u"R",
+    u"S",
+    u"T",
+    u"U",
+    u"V",
+    u"W",
+    u"X",
+    u"Y",
+    u"Z",
+    u"\u00c4",
+    u"\u00d6",
+    u"\u00d1",
+    u"\u00dc",
+    u"\u00a7",
+
+    u"\u00bf",
+    u"a",
+    u"b",
+    u"c",
+    u"d",
+    u"e",
+    u"f",
+    u"g",
+    u"h",
+    u"i",
+    u"j",
+    u"k",
+    u"l",
+    u"m",
+    u"n",
+    u"o",
+
+    u"p",
+    u"q",
+    u"r",
+    u"s",
+    u"t",
+    u"u",
+    u"v",
+    u"w",
+    u"x",
+    u"y",
+    u"z",
+    u"\u00e4",
+    u"\u00f6",
+    u"\u00f1",
+    u"\u00fc",
+    u"\u00e0"
+]
+
+
+def decode(s, n):
+    """
+    Decodes the given string using the given cell broadcast data coding scheme.
+    
+    @param s: string to decode
+    @param n: GSM cell broadcast data coding scheme
+    @return: UTF-8 string
+    """
+
+    # separate into nibbles
+    hbits = (n & 0xf0) >> 4
+    lbits = (n & 0x0f)
+    
+    if (hbits == 0x0):
+        # language
+        return _decode_language(s, lbits)
+
+    elif (0x1 <= hbits <= 0x3):
+        # reserved language
+        return s
+        
+    elif (0x4 <= hbits <= 0x7):
+        # general data coding indication
+        return _decode_general_data_coding(s, hbits, lbits)
+        
+    elif (0x8 <= hbits <= 0xe):
+        # reserved coding group
+        return s
+        
+    elif (hbits == 0xf):
+        # data coding / message handling
+        return s
+
+
+def _decode_language(s, lang):
+
+    return _decode_default_alphabet(s)
+
+
+def _decode_default_alphabet(s):
+    
+    # ought to be all in the 7 bit GSM character map
+    # modem is in 8 bit mode, so it makes 7 bit unpacking itself
+    chars = [ GSM_DEFAULT_ALPHABET[ord(c)] for c in s ]
+    u_str = "".join(chars)
+    return u_str.encode("utf-8")
+
+
+def _decode_hex(s):
+
+    return s.decode("hex")
+
+
+def _decode_usc2(s):
+
+    return s.decode("hex").decode("utf-16-be").encode("utf-8")
+
+
+def _decode_general_data_coding(s, h, l):
+
+    is_compressed = (h & 0x2)
+    
+    alphabet = (l & 0xc) >> 2
+
+    if (alphabet == 0x0):
+        # default alphabet
+        return _decode_default_alphabet(s)
+        
+    elif (alphabet == 0x1):
+        # 8 bit
+        # actually, encoding is user-defined, but let's assume hex'd ASCII
+        # for now
+        return _decode_hex(s)
+        
+    elif (alphabet == 0x2):
+        # USC2 (16 bit, BE)
+        return _decode_usc2(s)
+    elif (alphabet == 0x3):
+        # reserved
+        return s
+
+def decode_number(number):
+       dnumber = ""
+       for i in number:
+               if i & 0xf < 10:
+                       dnumber += str(int((i & 0xf)))
+               if (i & 0xf0) >> 4 < 10:
+                       dnumber += str(int((i & 0xf0) >> 4))
+       return dnumber
+
+def decode_timestamp(timestamp):
+       res = {}
+       res['year'] =  str(timestamp[0] & 0xf) + str((timestamp[0] & 0xf0) >> 4)
+       res['month'] = str(timestamp[1] & 0xf) + str((timestamp[1] & 0xf0) >> 4)
+       res['day'] = str(timestamp[2] & 0xf) + str((timestamp[2] & 0xf0) >> 4)
+       res['hour'] = str(timestamp[3] & 0xf) + str((timestamp[3] & 0xf0) >> 4)
+       res['minute'] = str(timestamp[4] & 0xf) + str((timestamp[4] & 0xf0) >> 4)
+       res['second'] = str(timestamp[5] & 0xf) + str((timestamp[5] & 0xf0) >> 4)
+       res['timezone'] = str(timestamp[6] & 0xf) + str((timestamp[6] & 0xf0) >> 4)
+       return res
+
+def decode_pdu (pdumsg):
+       pdu = {}
+       pdu['type'] = int(pdumsg[0])
+       if pdu['type'] & 0x3 == 0x0:
+               pdu['address_len'] = int(pdumsg[1])
+               pdu['type_of_address'] = int(pdumsg[2])
+               base = 3+(pdu['address_len']+1)/2
+               pdu['sender'] = decode_number(pdumsg[3:base])
+               pdu['pid'] = int(pdumsg[base])
+               pdu['dcs'] = int(pdumsg[base+1])
+               pdu['timestamp'] = decode_timestamp (pdumsg[base+2:base+9]);
+               pdu['udl'] = int(pdumsg[base+9])
+               pdu['user_data'] = pdumsg[base+10:len(pdumsg)]
+
+               alphabet = (pdu['dcs'] & 0xc) >> 2
+               if alphabet == 0x0:
+                       # This is 7-bit data. Taking of only pdu['udl'] bytes is important because we can't distinguish 0 at the end from @ if only one bit is used in last bite
+                       pdu['user_data'] = _decode_default_alphabet(deoctify(pdu['user_data']))[0:pdu['udl']]
+               elif alphabet == 0x1:
+                       # actually, encoding is user-defined, but let's assume ASCII
+                       pdu['user_data'] = ''.join([chr(i) for i in pdu['user_data']])
+               elif (alphabet == 0x2):
+                       # USC2 (16 bit, BE)
+                       pdu['user_data'] = (''.join([chr(i) for i in pdu['user_data'][6:len(pdu['user_data'])]])).decode("utf-16-be").encode("utf-8")
+               elif (alphabet == 0x3):
+                       # reserved
+                       pdu['user_data'] = ''.join([chr(i) for i in pdu['user_data']])
+
+               if pdu['type'] & 0x4 == 0:
+                       pdu['part'] = True
+               else:
+                       pdu['part'] = False
+       else:
+               # TODO support other types of messages
+               # This is not incoming message
+               # pdu['type'] & 0x3 == 2 means delivery report
+               return None
+
+       return pdu
+
diff --git a/ussd4all/ussdquery/ussdquery.py b/ussd4all/ussdquery/ussdquery.py
new file mode 100755 (executable)
index 0000000..2705dad
--- /dev/null
@@ -0,0 +1,337 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+## 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; version 2 and higer.
+##
+## Guseynov Alexey (kibergus bark-bark gmail.com) 2010
+
+import pexpect
+import time
+from subprocess import *
+import sys
+import gsmdecode
+import re
+import fcntl
+import os
+import stat
+import dbus
+import gobject
+
+# Needed for correct output of utf-8 symbols.
+sys.stdout=file("/dev/stdout", "wb")
+
+def check_number(number):
+       if number == "":
+               return False
+       for s in number :
+               if not (s in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "*", "#"]) :
+                       return False
+       return True
+
+if len(sys.argv) == 1:
+    print "Usage:\nussdquery.py <ussd number> [options]\nussdquery.py interactive [options]\n"+\
+"Options:\n-l language. Allowed languages: German, English, Italian, French, Spanish, Dutch, Swedish, Danish, Portuguese, Finnish, Norwegian, Greek, Turkish, Reserved1, Reserved2, Unspecified\n"+\
+"-r retry count. 0 default. Use -1 for infinite.\n-f If specified, errors, which occur on last query are threated as fatal\n"+\
+"-t timeout in seconds. Default 30. Timeout is considered to be critical error because you can't be sure answer for what request was returned.\n"+\
+"-d delimeter. Default is '\\n> \n'"+\
+"-m gain modem lock imidiately '"+\
+"-s show GUI message box on last reply"+\
+"For USSD menu navigation divide USSD number via spacebars for every next menu selection. Type exit in interactive mode to exit."
+    sys.exit()
+
+lockf = None
+
+def gain_lock ():
+       global lockf
+       lockf = open("/tmp/ussdquery.lock", 'a')
+       fcntl.flock(lockf,fcntl.LOCK_EX)
+       try:
+               os.chmod("/tmp/ussdquery.lock", stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
+       except:
+               None
+
+def release_lock ():
+       global lockf
+       fcntl.flock(lockf,fcntl.LOCK_UN)
+       lockf.close()
+
+def init_modem(modem):
+       # We have only one modem, simultaneous acces wouldn't bring anything good
+       gain_lock()
+       response = ""
+       init_retry = 5
+       while response != "OK" and init_retry > 0 :
+               if modem == None :
+                       # OK response should be recieved shortly
+                       modem = pexpect.spawn('pnatd', [], 2)
+               try :
+                       modem.send('at\r');
+                       # Read our "at" command
+                       modem.readline();
+                       # Read OK response
+                       response = modem.readline().strip()
+               except pexpect.TIMEOUT:
+                       modem.kill(9)
+                       modem = None
+                       response = ""
+               if response != "OK" :
+                       time.sleep(0.5)
+                       init_retry -= 1
+               else:
+                       try:
+                               # Switch output encoding to GSM default encoding
+                               modem.send('at+cscs="GSM"\r');
+                               # Read our command
+                               modem.readline();
+                               # Read OK response
+                               response = modem.readline().strip()
+                       except pexpect.TIMEOUT:
+                               modem.kill(9)
+                               modem = None
+                               response = ""
+                       
+               if response != "OK" :
+                       time.sleep(0.5)
+                       init_retry -= 1
+
+       if response != "OK" :
+               print >> sys.stderr, "Couldn't init modem."
+               if modem != None:
+                       modem.kill(9)
+               release_lock()
+               sys.exit (-1)
+
+       modem.timeout = timeout
+       return modem
+
+def close_modem (modem):
+       modem.sendeof()
+       modem.kill(9)
+       release_lock()
+
+retry = 0
+allow_last_error = True
+delimiter = "\n> "
+language = 15
+timeout = 30
+show_qussd = False
+
+if sys.argv[1] == "interactive":
+       number = "interactive"
+else:
+       number = sys.argv[1].split(" ")
+       for n in number: 
+               if not check_number(n):
+                       print >> sys.stderr, "Syntax error in USSD number."
+                       sys.exit(-7)
+
+modem = None
+
+# Parsing command line options
+arg = 1
+state = "arg"
+while arg < len(sys.argv)-1:
+       arg += 1
+       if state == "arg":
+               if sys.argv[arg] == "-l":
+                       state = "lang"
+                       continue
+               if sys.argv[arg] == "-r":
+                       state = "retry"
+                       continue
+               if sys.argv[arg] == "-t":
+                       state = "timeout"
+                       continue
+               if sys.argv[arg] == "-d":
+                       state = "delim"
+                       continue
+               if sys.argv[arg] == "-f":
+                       allow_last_error = False
+                       continue
+               if sys.argv[arg] == "-s":
+                       show_qussd = True
+                       continue
+               if sys.argv[arg] == "-m":
+                       modem = init_modem(modem)
+                       continue
+
+       if state == "lang":
+               if sys.argv[arg] == "German":
+                       language = 0
+               elif sys.argv[arg] == "English":
+                       language = 1
+               elif sys.argv[arg] == "Italian":
+                       language = 2
+               elif sys.argv[arg] == "French":
+                       language = 3
+               elif sys.argv[arg] == "Spanish":
+                       language = 4
+               elif sys.argv[arg] == "Dutch":
+                       language = 5
+               elif sys.argv[arg] == "Swedish":
+                       language = 6
+               elif sys.argv[arg] == "Danish":
+                       language = 7
+               elif sys.argv[arg] == "Portuguese":
+                       language = 8
+               elif sys.argv[arg] == "Finnish":
+                       language = 9
+               elif sys.argv[arg] == "Norwegian":
+                       language = 10
+               elif sys.argv[arg] == "Greek":
+                       language = 11
+               elif sys.argv[arg] == "Turkish":
+                       language = 12
+               elif sys.argv[arg] == "Reserved1":
+                       language = 13
+               elif sys.argv[arg] == "Reserved2":
+                       language = 14
+               elif sys.argv[arg] == "Unspecified":
+                       language = 15
+               else:
+                       print >> sys.stderr, "Language unknown, falling back to unspecified."
+               state = "arg"
+               continue
+
+       if state == "delim":
+               if number == "interactive":
+                       delimiter = sys.argv[arg]
+               else:
+                       print >> sys.stderr, "Delimiter is only supported in interactive mode."
+               state = "arg"
+               continue
+
+       if state == "retry":
+               if number == "interactive":
+                       print >> sys.stderr, "Retry is only supported in normal mode."
+               else:
+                       try:
+                               retry = int(sys.argv[arg])
+                               if retry < -1:
+                                       print >> sys.stderr, "Number of allowed errors must be >= -1. -1 assumed."
+                               retry = -1
+                       except:
+                               print >> sys.stderr, "Retry must be an integer."
+                               sys.exit(-5)
+               state = "arg"
+               continue
+       if state == "timeout":
+               try:
+                       timeout = int(sys.argv[arg])
+               except:
+                       print >> sys.stderr, "Timeout must be an integer."
+                       sys.exit(-5)
+               state = "arg"
+               continue
+
+       print >> sys.stderr, "Unrecogmized argument: "+sys.argv[arg]
+       
+if retry == -1:
+       retry_forever = True
+else:
+       retry_forever = False
+
+bus = dbus.SystemBus()
+ussdd = bus.get_object("su.kibergus.ussdd", "/su/kibergus/ussdd")
+ussdd_int = dbus.Interface(ussdd, "su.kibergus.ussdd")
+
+# Now we are ready to send commands
+
+stage = 0
+if number == "interactive":
+       sys.stdout.write(delimiter)
+       sys.stdout.flush()
+while number == "interactive" or stage < len(number):
+       if number == "interactive":
+               cnumber = sys.stdin.readline().strip()
+               if cnumber == "exit":
+                       if modem != None:
+                               close_modem (modem)
+                       sys.exit (0)
+               if not check_number (cnumber):
+                       sys.stdout.write ("Syntax error in USSD number"+delimiter)
+                       sys.stdout.flush()
+                       continue
+       else:
+               cnumber = number[stage]
+
+       if modem == None:
+               modem = init_modem(modem)
+
+       if retry == -1 and not retry_forever:
+               print >> sys.stderr, "Retry limit is over. Giving up."
+               break
+
+       try :
+               if number != "interactive" and not show_qussd or stage != len(number)-1:
+                       ussdd_int.skip_next()
+
+               modem.send('at+cusd=1,"'+cnumber+'",'+str(language)+'\r')
+               # Read our query echoed back
+               modem.readline()
+
+               #Read and parse reply
+               replystring = modem.readline().decode('string_escape')
+               # This will read out unneeded info from modem
+               modem.readline()
+               modem.readline()
+       except pexpect.TIMEOUT:
+               print >> sys.stderr, "Timeout. Modem didn't reply."
+               close_modem (modem)
+               ussdd_int.show_next()
+               sys.exit (-2)
+
+       if replystring.strip() == "ERROR" :
+               retry -= 1
+               print >> sys.stderr, "Modem returned ERROR. Query not executed."
+               ussdd_int.show_next()
+               continue
+
+       try:
+               reresult = re.match("(?s)^\\+CUSD: (\\d+),\"(.*)\",(\\d+)$", replystring.strip())
+
+               # 0 no further user action required (network initiated USSD-Notify, or no further information needed after mobile initiated operation)
+               # 1 further user action required (network initiated USSD-Request, or further information needed after mobile initiated operation)
+               # 2 USSD terminated by network
+               # 3 other local client has responded
+               # 4 operation not supported
+               # 5 network time out
+               reply_status=reresult.group(1)
+               reply = reresult.group(2)
+               # See GSM 07.07 and GSM 03.38
+               encoding = reresult.group(3)
+       except:
+               retry -= 1
+               print >> sys.stderr, "Couldn't parse modem answer: "+replystring
+               continue
+
+       if reply_status == 0:
+               # May be somebody else needs it
+               close_modem(modem)
+               modem = None
+       elif reply_status == 2:
+               print >> sys.stderr, "USSD terminated by network."
+        elif reply_status == 3:
+                print >> sys.stderr, "Error: other local client has responded."
+        elif reply_status == 4:
+                print >> sys.stderr, "Operation not supported."
+        elif reply_status == 5:
+                print >> sys.stderr, "Network time out."
+
+       # Decoding ansver
+       reply = gsmdecode.decode(reply, int(encoding))
+
+       if number == "interactive":
+               # prints line feed
+               sys.stdout.write(reply+delimiter)
+               sys.stdout.flush()
+       else:
+               if stage == len(number)-1:
+                       print reply
+       stage += 1
+       if not allow_last_error and namber != "interactive" and stage == len(number) - 1:
+               retry = 0
+
+if modem != None:
+       close_modem (modem)