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