]> git.neil.brown.name Git - plato.git/blob - gsm/gsmd.py
e1980d93e07ec0e41e31231cfa34f130b38d59dd
[plato.git] / gsm / gsmd.py
1 #!/usr/bin/env python
2
3 #
4 # Calls can be made by writing a number to
5 #  /run/gsm-state/call
6 # Status get set call 'Calling' and then 'BUSY' or ''
7 # Call can be answered by writing 'answer' to 'call'
8 # or can be cancelled by writing ''.
9 # During a call, chars can be written to
10 #  /run/gsm-state/dtmf
11 # to send tones.
12
13 ## FIXME
14 # e.g. receive AT response +CREG: 1,"08A7","6E48"
15 #  show that SIM is now ready
16 # cope with /var/lock/suspend not existing yet
17 #  define 'reset'
18
19 import re, time, gobject, os
20 from atchan import AtChannel
21 import dnotify, suspend
22 from tracing import log
23 from subprocess import Popen
24
25 def record(key, value):
26     f = open('/run/gsm-state/.new.' + key, 'w')
27     f.write(value)
28     f.close()
29     os.rename('/run/gsm-state/.new.' + key,
30               '/run/gsm-state/' + key)
31
32 def recall(key, nofile = ""):
33     try:
34         fd = open("/run/gsm-state/" + key)
35         l = fd.read(1000)
36         l = l.strip()
37         fd.close()
38     except IOError:
39         l = nofile
40     return l
41
42 def set_alert(key, value):
43     path = '/run/alert/' + key
44     if value == None:
45         try:
46             os.unlink(path)
47         except OSError:
48             pass
49     else:
50         try:
51             f = open(path, 'w')
52             f.write(value)
53             f.close()
54         except IOError:
55             pass
56
57 lastlog={}
58 def calllog(key, msg):
59     f = open('/var/log/' + key, 'a')
60     now = time.strftime("%Y-%m-%d %H:%M:%S")
61     f.write(now + ' ' + msg + "\n")
62     f.close()
63     lastlog[key] = msg
64
65 def calllog_end(key):
66     if key in lastlog:
67         calllog(key, '-end-')
68         del lastlog[key]
69
70 class Task:
71     def __init__(self, repeat):
72         self.repeat = repeat
73         pass
74     def start(self, channel):
75         # take the first action for this task
76         pass
77     def takeline(self, channel, line):
78         # a line has arrived that is presumably for us
79         pass
80     def timeout(self, channel):
81         # we asked for a timeout and got it
82         pass
83
84 class AtAction(Task):
85     # An AtAction involves:
86     #   optionally sending an AT command to check some value
87     #      matching the result against a string, possibly storing the value
88     #   if there is no match send some other AT command, probably to set a value
89     #
90     # States are 'init' 'checking', 'setting', 'done'
91     ok = re.compile("^OK")
92     busy = re.compile("\+CMS ERROR.*SIM busy")
93     not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
94     def __init__(self, check = None, ok = None, record = None, at = None,
95                  timeout=None, handle = None, repeat = None, arg = None,
96                  critical = True, noreply = None, retries = 5):
97         Task.__init__(self, repeat)
98         self.check = check
99         self.okstr = ok
100         if ok:
101             self.okre = re.compile(ok)
102         self.record = record
103         self.at = at
104         self.arg = arg
105         self.retries = retries
106         self.timeout_time = timeout
107         self.handle = handle
108         self.critical = critical
109         self.noreply = noreply
110
111     def start(self, channel):
112         channel.state['retries'] = 0
113         channel.state['stage'] = 'init'
114         self.advance(channel)
115
116     def takeline(self, channel, line):
117         if line == None:
118             channel.force_state('reset')
119             channel.advance()
120             return
121         m = self.ok.match(line)
122         if m:
123             channel.cancel_timeout()
124             if self.handle:
125                 self.handle(channel, line, None)
126             return self.advance(channel)
127
128         if self.busy.match(line):
129             channel.cancel_timeout()
130             channel.set_timeout(5000)
131             return
132         if self.not_ok.match(line):
133             return channel.abort_timeout()
134
135         if channel.state['stage'] == 'checking':
136             m = self.okre.match(line)
137             if m:
138                 channel.state['matched'] = True
139                 if self.record:
140                     record(self.record[0], m.expand(self.record[1]))
141                     if len(self.record) > 3:
142                         record(self.record[2], m.expand(self.record[3]))
143                 if self.handle:
144                     self.handle(channel, line, m)
145                 return
146
147         if channel.state['stage'] == 'setting':
148             # didn't really expect anything here..
149             pass
150
151     def timeout(self, channel):
152         if channel.state['retries'] >= self.retries:
153             if self.critical:
154                 channel.force_state('reset')
155             channel.advance()
156             return
157         channel.state['retries'] += 1
158         channel.state['stage'] = 'init'
159         channel.atcmd('')
160
161     def advance(self, channel):
162         st = channel.state['stage']
163         if st == 'init' and self.check:
164             channel.state['stage'] = 'checking'
165             if self.timeout_time:
166                 channel.atcmd(self.check, timeout = self.timeout_time)
167             else:
168                 channel.atcmd(self.check)
169         elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
170             channel.state['stage'] = 'setting'
171             at = self.at
172             if self.arg:
173                 at = at % channel.args[self.arg]
174             if self.timeout_time:
175                 channel.atcmd(at, timeout = self.timeout_time)
176             else:
177                 channel.atcmd(at)
178             if self.noreply:
179                 channel.cancel_timeout()
180                 channel.advance()
181         else:
182             channel.advance()
183
184 class PowerAction(Task):
185     # A PowerAction ensure that we have a connection to the modem
186     #  and sets the power on or off, or resets the modem
187     def __init__(self, cmd):
188         Task.__init__(self, None)
189         self.cmd = cmd
190
191     def start(self, channel):
192         if self.cmd == "on":
193             if not channel.connected:
194                 channel.connect()
195             if not channel.altchan.connected:
196                 channel.altchan.connect()
197             channel.check_flightmode()
198         elif self.cmd == "off":
199             record('carrier', '')
200             record('cell', '')
201             record('signal_strength','0/32')
202             channel.disconnect()
203             channel.altchan.disconnect()
204         elif self.cmd == 'reopen':
205             channel.disconnect()
206             channel.altchan.disconnect()
207             channel.connect()
208             channel.altchan.connect()
209         return channel.advance()
210
211 class ChangeStateAction(Task):
212     # This action changes to a new state, like a goto
213     def __init__(self, state):
214         Task.__init__(self, None)
215         self.newstate = state
216     def start(self, channel):
217         if self.newstate:
218             state = self.newstate
219         elif channel.nextstate:
220             state = channel.nextstate.pop(0)
221         else:
222             state = None
223         if state:
224             channel.gstate = state
225             channel.tasknum = None
226             log("ChangeStateAction chooses", channel.gstate)
227             n = len(control[channel.gstate])
228             channel.lastrun = n * [0]
229         return channel.advance()
230
231 class CheckSMS(Task):
232     def __init__(self):
233         Task.__init__(self, None)
234     def start(self, channel):
235         if 'incoming' in channel.nextstate:
236             # now is not a good time
237             return channel.advance()
238         if channel.pending_sms:
239             channel.pending_sms = False
240             p = Popen('gsm-getsms -n', shell=True, close_fds = True)
241             ok = p.wait()
242         return channel.advance()
243
244 class RouteVoice(Task):
245     def __init__(self, on):
246         Task.__init__(self, None)
247         self.request = on
248     def start(self, channel):
249         if self.request:
250             channel.sound_on = True
251             try:
252                 f = open("/run/sound/00-voicecall","w")
253                 f.close()
254             except:
255                 pass
256             p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True)
257             log('Running gsm-voice-routing pid', p.pid)
258             channel.voice_route = p
259         elif channel.sound_on:
260             if channel.voice_route:
261                 channel.voice_route.send_signal(15)
262                 channel.voice_route.wait()
263                 channel.voice_route = None
264             try:
265                 os.unlink("/run/sound/00-voicecall")
266             except OSError:
267                 pass
268             channel.sound_on = False
269         return channel.advance()
270
271 class BlockSuspendAction(Task):
272     def __init__(self, enable):
273         Task.__init__(self, None)
274         self.enable = enable
275     def start(self, channel):
276         print "BlockSuspendAction sets", self.enable
277         if self.enable:
278             channel.suspend_blocker.block()
279             # No point holding a pending suspend any more
280             if channel.suspend_pending:
281                 channel.suspend_pending = False
282                 print "BlockSuspendAction calls release"
283                 suspend.abort_cycle()
284                 channel.suspend_handle.release()
285         if not self.enable:
286             channel.suspend_blocker.unblock()
287
288         channel.advance()
289
290 class Async:
291     def __init__(self, msg, handle, handle_extra = None):
292         self.msg = msg
293         self.msgre = re.compile(msg)
294         self.handle = handle
295         self.handle_extra = handle_extra
296
297     def match(self, line):
298         return self.msgre.match(line)
299
300 # async handlers...
301 LAC=0
302 CELLID=0
303 cellnames={}
304 def status_update(channel, line, m):
305     if m and m.groups()[3] != None:
306         global LAC, CELLID, cellnames
307         LAC = int(m.groups()[2],16)
308         CELLID = int(m.groups()[3],16)
309         record('cellid', "%04X %06X" % (LAC, CELLID));
310         if CELLID in cellnames:
311             record('cell', cellnames[CELLID])
312             log("That one is", cellnames[CELLID])
313
314 def new_sms(channel, line, m):
315     if m:
316         channel.pending_sms = False
317         record('newsms', m.groups()[1])
318         p = Popen('gsm-getsms -n', shell=True, close_fds = True)
319         ok = p.wait()
320
321 def maybe_sms(line, channel):
322     channel.pending_sms = True
323
324 def sigstr(channel, line, m):
325     if m:
326         record('signal_strength', m.groups()[0] + '/32')
327
328 global incoming_cell_id
329 def cellid_update(channel, line, m):
330     # get something like +CBM: 1568,50,1,1,1
331     # don't know what that means, just collect the 'extra' line
332     # I think the '50' means 'this is a cell id'.  I should
333     # probably test for that.
334     #
335     # response can be multi-line
336     global incoming_cell_id
337     incoming_cell_id = ""
338
339 def cellid_new(channel, line):
340     global CELLID, cellnames, incoming_cell_id
341     if not line:
342         # end of message
343         if incoming_cell_id:
344             l = re.sub('[^!-~]+',' ',incoming_cell_id)
345             if CELLID:
346                 cellnames[CELLID] = l
347             record('cell', l)
348             return False
349     line = line.strip()
350     if incoming_cell_id:
351         incoming_cell_id += ' ' + line
352     else:
353         incoming_cell_id = line
354     return True
355
356 incoming_num = None
357 def incoming(channel, line, m):
358     global incoming_num
359     if incoming_num:
360         record('incoming', incoming_num)
361     else:
362         record('incoming', '-')
363     set_alert('ring', 'new')
364     if channel.gstate not in ['on-call', 'incoming', 'answer']:
365         calllog('incoming', '-call-')
366         channel.set_state('incoming')
367         record('status', 'INCOMING')
368         global cpas_zero_cnt
369         cpas_zero_cnt = 0
370
371 def incoming_number(channel, line, m):
372     global incoming_num
373     if m:
374         num = m.groups()[0]
375         if incoming_num == None:
376             calllog('incoming', num);
377         incoming_num = num
378         record('incoming', incoming_num)
379
380 def no_carrier(channel, line, m):
381     record('status', '')
382     record('call', '')
383     if channel.gstate != 'idle':
384         channel.set_state('idle')
385
386 def busy(channel, line, m):
387     record('status', 'BUSY')
388     record('call', '')
389
390 def ussd(channel, line, m):
391     pass
392
393 cpas_zero_cnt = 0
394 def call_status(channel, line, m):
395     global cpas_zero_cnt
396     global calling
397     log("call_status got", line)
398     if not m:
399         return
400     s = int(m.groups()[0])
401     log("s = %d" % s)
402     if s == 0:
403         if calling:
404             return
405         cpas_zero_cnt += 1
406         if cpas_zero_cnt <= 3:
407             return
408         # idle
409         global incoming_num
410         incoming_num = None
411         record('incoming', '')
412         if channel.gstate in ['on-call','incoming','call']:
413             calllog_end('incoming')
414             calllog_end('outgoing')
415             record('status', '')
416         if channel.gstate != 'idle' and channel.gstate != 'suspend':
417             channel.set_state('idle')
418     cpas_zero_cnt = 0
419     calling = False
420     if s == 3:
421         # incoming call
422         if channel.gstate not in  ['incoming', 'answer']:
423             # strange ..
424             channel.set_state('incoming')
425             record('status', 'INCOMING')
426             set_alert('ring', 'new')
427             record('incoming', '-')
428     if s == 4:
429         # on a call - but could be just a data call, so don't do anything
430         #if channel.gstate != 'on-call' and channel.gstate != 'hangup':
431         #    channel.set_state('on-call')
432         pass
433
434 def check_cfun(channel, line, m):
435     # response to +CFUN?
436     # If '1', then advance from init1 to init2
437     # else if not 0, possibly do a reset
438     if not m:
439         return
440     if m.groups()[0] == '1':
441         if channel.gstate == 'init1':
442             channel.set_state('init2')
443         return
444     if m.groups()[0] != '0':
445         if channel.last_reset + 100 < time.time():
446             channel.last_reset = time.time()
447             channel.set_state('reset')
448         return
449
450 def data_handle(channel, line, m):
451     # Response to _OWANDATA - should contain IP address etc
452     if not m:
453         if 'matched' in channel.state:
454             # already handled match
455             return
456         # no connection active
457         data_hungup(channel, failed=False)
458         return
459     # m[0] is gateway/IP addr.  m[1] and m[2] are DNS servers
460     dns = (m.groups()[1], m.groups()[2])
461     if channel.data_DNS != dns:
462         record('dns', '%s %s' % dns)
463         channel.data_DNS = dns
464     ip = m.groups()[0]
465     if channel.data_IP != ip:
466         channel.data_IP = ip
467         os.system('/sbin/ifconfig hso0 up %s' % ip)
468         record('data', ip)
469     data_log_update()
470
471 def data_call(channel, line, m):
472     # delayed reponse to _OWANCALL.  Maybe be async, may be
473     # polled with '_OWANCALL?'
474     if not m:
475         return
476     if m.groups()[0] != '1':
477         return
478     s = int(m.groups()[1])
479     #   0 = Disconnected, 1 = Connected, 2 = In setup,  3 = Call setup failed.
480     if s == 0:
481         data_hungup(channel, failed=False)
482     elif s == 1:
483         channel.set_state('idle')
484     elif s == 2:
485         # try again soon
486         pass
487     elif s == 3:
488         data_hungup(channel, failed=True)
489
490 def data_hungup(channel, failed):
491     if channel.data_IP:
492         os.system('/sbin/ifconfig hso0 0.0.0.0 down')
493     record('dns', '')
494     record('data', '')
495     channel.data_IP = None
496     channel.data_DNS = None
497     # FIXME should I retry, or reset?
498     if channel.data_APN:
499         # We still want a connection
500         if failed:
501             channel.set_state('reset')
502             return
503         elif channel.next_data_call <= time.time():
504             channel.next_data_call = (time.time() +
505                                       time.time() - channel.last_data_call);
506             channel.last_data_call = time.time()
507             data_log_reset()
508             channel.set_state('data-call')
509             return
510     if channel.gstate == 'data-call':
511         channel.set_state('idle')
512
513 # DATA traffic logging - goes to /var/log/gsm-data
514 def read_bytes(dev = 'hso0'):
515     f = file('/proc/net/dev')
516     rv = None
517     for l in f:
518         w = l.strip().split()
519         if w[0] == dev + ':':
520             rv = ( int(w[1]), int(w[9]) )
521             break
522     f.close()
523     return rv
524
525 last_data_usage = None
526 last_data_time = 0
527 SIM = None
528 def data_log_reset():
529     global last_data_usage, last_data_time
530     last_data_usage = read_bytes()
531     last_data_time = time.time()
532     record('data-last-usage', '%s %s' % last_data_usage)
533
534 def data_log_update(force = False):
535     global last_data_usage, last_data_time, SIM
536
537     if not SIM:
538         SIM = recall('sim')
539     if not SIM:
540         return
541     if not last_data_usage:
542         data_log_reset()
543
544     if not force and time.time() - last_data_time < 10*60:
545         return
546
547     data_usage = read_bytes()
548
549     calllog('gsm-data', '%s %d %d' %
550             (SIM,
551              data_usage[0] - last_data_usage[0],
552              data_usage[1] - last_data_usage[1]))
553
554     last_data_usage = data_usage
555     last_data_time = time.time()
556     record('data-last-usage', '%s %s' % last_data_usage)
557
558 control = {}
559
560 # For flight mode, we turn the power off.
561 control['to-flight'] = [
562     AtAction(at='+CFUN=0'),
563     PowerAction('off'),
564     ChangeStateAction('flight')
565 ]
566 control['flight'] = [
567     BlockSuspendAction(False),
568     ]
569
570 control['reset'] = [
571     # turning power off just kills everything!!!
572     #AtAction(at='_ORESET', critical = False),
573     AtAction(at='$QCPWRDN', critical = False, retries = 0),
574     PowerAction('reopen'),
575     #PowerAction('off'),
576     AtAction(at='E0', timeout=30000),
577     ChangeStateAction('init1'),
578     ]
579
580 # For suspend, we want power on, but no wakups for status or cellid
581 control['suspend'] = [
582     AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
583     CheckSMS(),
584     ChangeStateAction(None), # allow async state change
585     AtAction(at='+CNMI=1,1,0,0,0'),
586     AtAction(at='_OSQI=0'),
587     AtAction(at='_OEANT=0'),
588     AtAction(at='_OSSYS=0'),
589     AtAction(at='_OPONI=0'),
590     AtAction(at='+CREG=0'),
591     ]
592 control['resume'] = [
593     BlockSuspendAction(True),
594     AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
595     AtAction(at='+CNMI=1,1,2,0,0', critical=False),
596     AtAction(at='_OSQI=1', critical=False),
597     AtAction(at='+CREG=2'),
598     CheckSMS(),
599     ChangeStateAction(None),
600     ChangeStateAction('idle'),
601     ]
602
603 control['listenerr'] = [
604     PowerAction('on'),
605     AtAction(at='V1E0'),
606     AtAction(at='+CMEE=2;+CRC=1')
607     ]
608
609 # init1 checks phone status and once we are online
610 # we switch to init2, then idle
611 control['init1'] = [
612     BlockSuspendAction(True),
613     PowerAction('on'),
614     AtAction(at='V1E0'),
615     AtAction(at='+CMEE=2;+CRC=1'),
616     # Turn the device on.
617     AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
618     # Report carrier as long name
619     AtAction(at='+COPS=3,0'),
620     # register with a carrier
621     #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
622     #         record=('carrier', '\\1'), timeout=10000),
623     AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
624              record=('carrier', '\\1'), timeout=10000),
625     AtAction(check='+CFUN?', ok='\+CFUN: (\d)', at='+CFUN=1', timeout=10000, handle=check_cfun, repeat=5000),
626 ]
627
628 control['init2'] = [
629     # text format for various messages such SMS
630     AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'),
631     # get location status updates
632     AtAction(at='+CREG=2'),
633     AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")',
634              handle=status_update, timeout=4000),
635     # Enable collection of  Cell Info message
636     #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
637     #AtAction(at='+CSCB=0'),
638     AtAction(at='+CSCB=1', critical=False),
639     # Enable async reporting of TXT and Cell info messages
640     #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
641     AtAction(at='+CNMI=1,0,0,0,0', critical=False),
642     AtAction(at='+CNMI=1,1,2,0,0', critical=False),
643     # Enable async reporting of signal strength
644     AtAction(at='_OSQI=1', critical=False),
645     AtAction(check='+CIMI', ok='(\d\d\d+)', record=('sim','\\1')),
646     #_OSIMOP: "YES OPTUS","YES OPTUS","50502"
647     AtAction(check='_OSIMOP', ok='_OSIMOP: "(.*)",".*","(.*)"',
648              record=('sid','\\2', 'carrier','\\1'), critical=False),
649
650     # Make sure to use both 2G and 3G
651     AtAction(at='_OPSYS=3,2', critical=False),
652
653     # Enable reporting of Caller number id.
654     AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000,
655              critical = False),
656     AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
657     ChangeStateAction('idle')
658     ]
659
660 def if_data(channel):
661     if not channel.data_APN and not channel.data_IP:
662         # no data happening
663         return 0
664     if channel.data_APN and channel.data_IP:
665         # connection is set up - slow watch
666         return 30000
667     if channel.data_IP:
668         # must be shutting down - poll quickly, it shouldn't take long
669         return 2000
670     # we want a connection but don't have one, so we retry
671     if time.time() < channel.next_data_call:
672         return int((channel.next_data_call - time.time()) * 1000)
673     return 1000
674
675 control['idle'] = [
676     RouteVoice(False),
677     CheckSMS(),
678     BlockSuspendAction(False),
679     AtAction(check='+CFUN?', ok='\+CFUN: (\d)', timeout=10000, handle=check_cfun, repeat=30000),
680     AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
681              record=('carrier', '\\1'), timeout=10000),
682     #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
683     #         record=('carrier', '\\1'), timeout=10000, repeat=37000),
684     # get signal string
685     AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
686              record=('signal_strength','\\1/32'), repeat=29000),
687     AtAction(check='_OWANDATA?',
688              ok='_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$',
689              handle=data_handle, repeat=if_data),
690     ]
691
692 control['data-call'] = [
693     AtAction(at='+CGDCONT=1,"IP","%s"', arg='APN'),
694     AtAction(at='_OWANCALL=1,1,0'),
695     AtAction(at='_OWANCALL?', handle=data_call, repeat=2000),
696     #ChangeStateAction('idle')
697 ]
698
699 control['data-hangup'] = [
700     AtAction(at='_OWANCALL=1,0,0'),
701     ChangeStateAction('idle')
702 ]
703
704 control['incoming'] = [
705     BlockSuspendAction(True),
706     AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
707
708     # monitor signal strength
709     AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
710              record=('signal_strength','\\1/32'), repeat=30000)
711     ]
712
713 control['answer'] = [
714     AtAction(at='A'),
715     RouteVoice(True),
716     ChangeStateAction('on-call')
717     ]
718
719 control['call'] = [
720     AtAction(at='D%s;', arg='number'),
721     RouteVoice(True),
722     ChangeStateAction('on-call')
723     ]
724
725 control['dtmf'] = [
726     AtAction(at='+VTS=%s', arg='dtmf', noreply=True),
727     ChangeStateAction('on-call')
728     ]
729
730 control['hangup'] = [
731     AtAction(at='+CHUP', critical=False, retries=0),
732     ChangeStateAction('idle')
733     ]
734
735 control['on-call'] = [
736     BlockSuspendAction(True),
737     AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
738
739     # get signal strength
740     AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
741              record=('signal_strength','\\1/32'), repeat=30000)
742     ]
743
744 async = [
745     Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
746     Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
747     Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
748           handle_extra = cellid_new),
749     Async(msg='\+CRING: (.*)', handle = incoming),
750     Async(msg='RING', handle = incoming),
751     Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number),
752     Async(msg='NO CARRIER', handle = no_carrier),
753     Async(msg='BUSY', handle = busy),
754     Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd),
755     Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr),
756
757     Async(msg='_OWANCALL: (\d), (\d)', handle = data_call),
758     ]
759
760 class GsmD(AtChannel):
761
762     # gsmd works like a state machine
763     # the high level states are: flight suspend idle incoming on-call
764     #   Note that the whole 'call-waiting' experience is not coverred here.
765     #     That needs to be handled by whoever answers calls and allows interaction
766     #     between user and phone system.
767     #
768     # Each state contains a list of tasks such as setting and
769     # checking config options and monitoring state (e.g. signal strength)
770     # Some tasks are single-shot and only need to complete each time the state is
771     # entered.  Others are repeating (such as status monitoring).
772     # We take the first task of the current list and execute it, or wait
773     # until one will be ready.
774     # Tasks themselves can be state machines, so we keep track of what 'stage'
775     # we are up to in the current task.
776     #
777     # The system is (naturally) event driven.  The main two events that we
778     # receive are:
779     # 'takeline' which presents one line of text from the GSM device, and
780     # 'timeout' which indicates that a timeout set when a command was sent has
781     # expired.
782     # Other events are:
783     #   'taskready'  when the time of the next pending task arrives.
784     #   'flight'     when the state of the 'flight mode' has changed
785     #   'suspend'    when a suspend has been requested.
786     #
787     # Each event does some event specific processing to modify the state,
788     # Then calls 'self.advance' to progress the state machine.
789     # When high level state changes are requested, any pending task is discarded.
790     #
791     # If a task detects an error (gsm device not responding properly) it might
792     # request a reset.  This involves sending a modem_reset command and then
793     # restarting the current state from the top.
794     # A task can also indicate:
795     #  The next stage to try
796     #  How long to wait before retrying (or None)
797     #
798
799     def __init__(self, path, altpath):
800         AtChannel.__init__(self, path = path)
801
802         self.extra = None
803         self.flightmode = True
804         self.state = None
805         self.args = {}
806         self.suspend_pending = False
807         self.pending_sms = False
808         self.sound_on = True
809         self.voice_route = None
810         self.tasknum = None
811         self.altpath = altpath
812         self.altchan = CarrierDetect(altpath, self)
813         self.data_APN = None
814         self.data_IP =  None
815         self.data_DNS = None
816         self.last_data_call = 0
817         self.next_data_call = 0
818         self.gstate = None
819         self.nextstate = []
820         self.last_reset = time.time()
821
822         record('carrier','')
823         record('cell','')
824         record('incoming','')
825         record('signal_strength','')
826         record('status', '')
827         record('sim','')
828         record('sid','')
829         record('data-APN', '')
830         record('data', '')
831         record('dns', '')
832
833         # set the initial state
834         self.set_state('flight')
835
836         # Monitor other external events which affect us
837         d = dnotify.dir('/var/lib/misc/flightmode')
838         self.flightmode_watcher = d.watch('active', self.check_flightmode)
839         d = dnotify.dir('/run/gsm-state')
840         self.call_watcher = d.watch('call', self.check_call)
841         self.dtmf_watcher = d.watch('dtmf', self.check_dtmf)
842         self.data_watcher = d.watch('data-APN', self.check_data)
843
844         self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
845         self.suspend_blocker = suspend.blocker()
846
847         # Check the externally imposed state
848         self.check_flightmode(self.flightmode_watcher)
849
850         # and GO!
851         self.advance()
852
853     def check_call(self, f = None):
854         l = recall('call')
855         log("Check call got", l)
856         if l == "":
857             if self.gstate != 'idle':
858                 global incoming_num
859                 incoming_num = None
860                 self.set_state('hangup')
861                 record('status','')
862                 record('incoming','')
863                 calllog_end('incoming')
864                 calllog_end('outgoing')
865         elif l == 'answer':
866             if self.gstate == 'incoming':
867                 record('status', 'on-call')
868                 record('incoming','')
869                 set_alert('ring', None)
870                 self.set_state('answer')
871         else:
872             if self.gstate == 'idle':
873                 global calling
874                 calling = True
875                 self.args['number'] = l
876                 self.set_state('call')
877                 calllog('outgoing',l)
878                 record('status', 'on-call')
879
880     def check_dtmf(self, f = None):
881         l = recall('dtmf')
882         log("Check dtmf got", l)
883         if len(l):
884             self.args['dtmf'] = l
885             self.set_state('dtmf')
886             record('dtmf','')
887
888     def check_data(self, f = None):
889         l = recall('data-APN')
890         log("Check data got", l)
891         if l == "":
892             l = None
893         if self.data_APN != l:
894             self.data_APN = l
895             self.args['APN'] = self.data_APN
896             if self.data_IP:
897                 self.set_state('data-hangup')
898                 data_log_update(True)
899             elif self.gstate == 'idle' and self.data_APN:
900                 self.last_data_call = time.time()
901                 self.next_data_call = time.time()
902                 data_log_reset()
903                 self.set_state('data-call')
904
905     def check_flightmode(self, f = None):
906         try:
907             fd = open("/var/lib/misc/flightmode/active")
908             l = fd.read(1)
909             fd.close()
910         except IOError:
911             l = ""
912         log("check flightmode got", len(l))
913         if len(l) == 0:
914             if self.flightmode:
915                 self.flightmode = False
916                 if self.suspend_handle.suspended:
917                     self.set_state('suspend')
918                 else:
919                     self.set_state('init1')
920         else:
921             if not self.flightmode:
922                 self.flightmode = True
923                 self.set_state('to-flight')
924
925     def do_suspend(self):
926         self.suspend_pending = True
927         if self.gstate not in ['flight', 'resume']:
928             print "do suspend sets suspend"
929             self.set_state('suspend')
930         else:
931             print "do suspend avoids suspend"
932             self.abort_timeout()
933         return False
934
935     def do_resume(self):
936         if self.gstate == 'suspend':
937             self.set_state('resume')
938
939     def set_state(self, state):
940         # this happens asynchronously so we must be careful
941         # about changing things.  Just record the new state
942         # and abort any timeout
943         if state == self.gstate or state in self.nextstate:
944             log("state already destined to be", state)
945             return
946         log("state should become", state)
947         self.nextstate.append(state)
948         self.abort_timeout()
949
950     def force_state(self, state):
951         # immediately go to new state - must be called carefully
952         log("Force state to", state);
953         self.cancel_timeout()
954         self.nextstate = []
955         n = len(control[state])
956         self.lastrun = n * [0]
957         self.tasknum = None
958         self.gstate = state
959
960     def advance(self):
961         # 'advance' is called by a 'Task' when it has finished
962         # It may have called 'set_state' first either to report
963         # an error or to effect a regular state change
964         now = int(time.time()*1000)
965         if self.tasknum != None:
966             self.lastrun[self.tasknum] = now
967             self.tasknum = None
968         (t, delay) = self.next_cmd()
969         log("advance %s chooses %d, %d" % (self.gstate, t, delay))
970         if delay and self.nextstate:
971             # time to effect 'set_state' synchronously
972             self.gstate = self.nextstate.pop(0)
973             log("state becomes", self.gstate)
974             n = len(control[self.gstate])
975             self.lastrun = n * [0]
976             t, delay = self.next_cmd()
977
978         if delay:
979             log("Sleeping for %f seconds" % (delay/1000.0))
980             self.set_timeout(delay)
981             if self.suspend_pending:
982                 # It is important that this comes after set_timeout
983                 # as we might get an abort_timeout as a result of the
984                 # release, and there needs to be a timeout to abort
985                 self.suspend_pending = False
986                 print "advance calls release"
987                 self.suspend_handle.release()
988         else:
989             self.tasknum = t
990             self.state = {}
991             control[self.gstate][t].start(self)
992
993     def takeline(self, line):
994
995         if self.extra:
996             # an async message is multi-line and we need to handle
997             # the extra line.
998             if not self.extra.handle_extra(self, line):
999                 self.extra = None
1000             return False
1001
1002         if line == None:
1003             self.force_state('reset')
1004             self.advance()
1005         if not line:
1006             return False
1007
1008         # Check for an async message
1009         for m in async:
1010             mt = m.match(line)
1011             if mt:
1012                 m.handle(self, line, mt)
1013                 if m.handle_extra:
1014                     self.extra = m
1015                 return False
1016
1017         # else pass it to the task
1018         if self.tasknum != None:
1019             control[self.gstate][self.tasknum].takeline(self, line)
1020
1021     def timedout(self):
1022         if self.tasknum == None:
1023             self.advance()
1024         else:
1025             control[self.gstate][self.tasknum].timeout(self)
1026
1027     def next_cmd(self):
1028         # Find a command to execute, or a delay
1029         # return (cmd,time)
1030         # cmd is an index into control[state],
1031         # time is seconds until try something
1032         mindelay = 60*60*1000
1033         if self.gstate == None:
1034             return (0, mindelay)
1035         cs = control[self.gstate]
1036         n = len(cs)
1037         now = int(time.time()*1000)
1038         for i in range(n):
1039             if self.lastrun[i] == 0:
1040                 return (i, 0)
1041             repeat = cs[i].repeat
1042             if repeat == None:
1043                 repeat = 0
1044             elif type(repeat) != int:
1045                 repeat = repeat(self)
1046
1047             if repeat and self.lastrun[i] + repeat <= now:
1048                 return (i, 0)
1049             if repeat:
1050                 delay = (self.lastrun[i] + repeat) - now;
1051                 if delay < mindelay:
1052                     mindelay = delay
1053         return (0, mindelay)
1054
1055 class CarrierDetect(AtChannel):
1056     # on the hso modem in the GTA04, the 'NO CARRIER' signal
1057     # arrives on the 'Modem' port, not on the 'Application' port.
1058     # So we listen to the 'Modem' port, and report any
1059     # 'NO CARRIER' we see - or indeed anything that we see.
1060     def __init__(self, path, main):
1061         AtChannel.__init__(self, path = path)
1062         self.main = main
1063
1064     def takeline(self, line):
1065         self.main.takeline(line)
1066
1067 class SysfsWatcher:
1068     # watch for changes on a sysfs file and report them
1069     # We read the content, report that, wait for a change
1070     # and report again
1071     def __init__(self, path, action):
1072         self.path = path
1073         self.action = action
1074         self.fd = open(path, "r")
1075         self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read)
1076         self.read()
1077
1078     def read(self, *args):
1079         self.fd.seek(0)
1080         try:
1081             r = self.fd.read(4096)
1082         except IOerror:
1083             return True
1084         self.action(r)
1085         return True
1086
1087 try:
1088     os.mkdir("/run/gsm-state")
1089 except:
1090     pass
1091
1092 calling = False
1093 a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem')
1094 print "GsmD started"
1095
1096 try:
1097     f = open("/sys/class/gpio/gpio176/edge", "w")
1098 except IOError:
1099     f = None
1100 if f:
1101     f.write("rising")
1102     f.close()
1103     w = SysfsWatcher("/sys/class/gpio/gpio176/value",
1104                      lambda l: maybe_sms(l, a))
1105 else:
1106     import evdev
1107     def check_evt(dc, mom, typ, code, val):
1108         if typ == 1 and val == 1:
1109             # keypress
1110             maybe_sms("", a)
1111     try:
1112         f = evdev.EvDev("/dev/input/incoming", check_evt)
1113     except:
1114         f = None
1115 c = gobject.main_context_default()
1116 while True:
1117     c.iteration()