]> git.neil.brown.name Git - plato.git/blob - gsm/gsmd2.py
gsmd2: register engine: only CFUN if we are 'on'.
[plato.git] / gsm / gsmd2.py
1 #!/usr/bin/env python
2
3
4 # Error cases to handle
5 # if +CFUN:6, then "+CFUN=1" can produce "+CME ERROR: operation not supported"
6 #   close/open sometimes fixes.
7 # +CIMI  can produce +CME ERROR: operation not allowed
8 #   close/open seems to fix.
9 # +CLIP? can produce CME ERROR: network rejected request
10 #   don't know what fixed it
11 # +CSCB=1 can produce +CMS ERROR: 500
12 #   just give up and retry later.
13 # CFUN:4 can be fixed by writing CFUN=1
14 # CFUN:6 needs close/open
15 # _OPSYS=3,2 can produce ERROR.  Just try much later I guess.
16
17 #TODO
18 # send sms
19 # USS
20 #Handle COPS
21 # repeat status until full success
22 # Keep suspend blocked while any messages are queued.
23 # Need to detect reset and reset configs
24 # use CLCC to get number
25
26 import gobject
27 import sys
28 import re, time, os
29 from atchan import AtChannel
30 import dnotify, suspend
31 from tracing import log
32 from subprocess import Popen
33 from evdev import EvDev
34 import wakealarm
35 import storesms
36 import sms
37
38 def safe_read(file, default=''):
39     try:
40         fd = open(file)
41         l = fd.read(1000)
42         l = l.strip()
43         fd.close()
44     except IOError:
45         l = default
46     return l
47
48 recording = {}
49 def record(key, value):
50     global recording
51     try:
52         f = open('/run/gsm-state/.new.' + key, 'w')
53         f.write(value)
54         f.close()
55         os.rename('/run/gsm-state/.new.' + key,
56                   '/run/gsm-state/' + key)
57     except OSError:
58         # I got this once on the rename, don't know why
59         pass
60     recording[key] = value
61
62 def recall(key, nofile = ""):
63     return safe_read("/run/gsm-state/" + key, nofile)
64
65 lastlog={}
66 def call_log(key, msg):
67     f = open('/var/log/' + key, 'a')
68     now = time.strftime("%Y-%m-%d %H:%M:%S")
69     f.write(now + ' ' + msg + "\n")
70     f.close()
71     lastlog[key] = msg
72
73 def call_log_end(key):
74     if key in lastlog:
75         call_log(key, '-end-')
76         del lastlog[key]
77
78 def set_alert(key, value):
79     path = '/run/alert/' + key
80     tpath = '/run/alert/.new.' + key
81     if value == None:
82         try:
83             os.unlink(path)
84         except OSError:
85             pass
86     else:
87         try:
88             f = open(tpath, 'w')
89             f.write(value)
90             f.close()
91             os.rename(tpath, path)
92         except IOError:
93             pass
94         suspend.abort_cycle()
95
96 def gpio_set(line, val):
97     file = "/sys/class/gpio/gpio%d/value" % line
98     try:
99         fd = open(file, "w")
100         fd.write("%d\n" % val)
101         fd.close()
102     except IOError:
103         pass
104
105 ##
106 # suspend handling:
107 # There are three ways we interact with suspend
108 #  1/ we block suspend when something important is happening:
109 #    - phone call active
110 #    - during initialisation?
111 #    - AT command with timeout longer than 10 seconds.
112 #  2/ on suspend request we performs some checks before allowing the suspend,
113 #    and send notificaitons  on resume
114 #    - If an AT command or async is pending, we don't ack the suspend request
115 #      until a reply comes.
116 #  3/ Some timeouts can wake up from suspend - e.g. CFUN poll.
117 #
118 # Each Engine can individually block suspend.  The number that are
119 # active is maintained separately and suspend is blocked when that number
120 # is positive.  On turn-off, everything is forced to allow suspend.
121 # When suspend is pending, "set_suspend" is called on each engine.
122 # An engine an return False to say that it doesn't want to suspend just now.
123 # It should have started blocking, or signalled something.
124 # Engines are informed for Resume when it happens.
125 #
126 # A 'retry' can have a non-resuming timeout and a resuming timeout.
127 # The resuming timeout should be set while suspend is blocked, but can be
128 # extended at other times.
129 #
130 # Important things should happen with a clear chain of suspend blocking.
131 # e.g. The 'incoming' must be checked in the presuspend handler and create a
132 # suspend block if needed.  That should be retained until txt messages are read
133 # or phone call completes.
134 # CFUN resuming timeout should block suspend until an answer is read and it is
135 # rescheduled.
136 # On startup we block until everyone has registerd the power-on. etc.
137 #
138 # - DONE flightmode
139 # - incoming
140 #   SMS
141 #   CALL
142 # - CFUN timer
143
144 # - I keep getting EOF - why is that?
145 # - double close is bad
146 # - modem delaying of suspend doesn't quite seem right.
147
148
149 class SuspendMan():
150     def __init__(self):
151         self.handle = suspend.blocker(False)
152         self.count = 0;
153     def block(self):
154         if self.count == 0:
155             self.handle.block()
156             self.handle.abort()
157         self.count += 1
158     def unblock(self):
159         self.count -= 1
160         if self.count == 0:
161             self.handle.unblock()
162 sus = SuspendMan()
163
164 class Engine:
165     def __init__(self):
166         self.timer = None
167         self.wakealarm = None
168         # delay is the default timeout for 'retry'
169         self.delay = 60000
170         # resuming_delay is the default timeout if we suspend
171         self.resuming_delay = None
172         # 'blocked' if true if we asked to block suspend.
173         self.blocked = False
174         # 'blocking' is a handle if we refused to let suspend continue yet.
175         self.blocking = None
176
177     def set_on(self, state):
178         pass
179     def set_service(self, state):
180         pass
181     def set_resume(self):
182         pass
183     def set_suspend(self):
184         return True
185
186     def retry(self, delay = None, resuming = None):
187         if self.timer:
188             gobject.source_remove(self.timer)
189             self.timer = None
190         if not delay is False:
191             if delay == None:
192                 delay = self.delay
193             self.timer = gobject.timeout_add(delay, self.call_retry)
194         if not resuming is False:
195             if resuming == None:
196                 resuming = self.resuming_delay
197             if resuming != None:
198                 when = time.time() + resuming
199                 if self.wakealarm and not self.wakealarm.s:
200                     self.wakealarm = None
201                 if self.wakealarm:
202                     self.wakealarm.realarm(when)
203                 else:
204                     self.wakealarm = wakealarm.wakealarm(when,
205                                                          self.wake_retry)
206         elif self.wakealarm:
207             self.wakealarm.release()
208             self.wakealarm = None
209
210     def wake_retry(self, handle):
211         self.wakealarm = None
212         self.do_retry()
213         return True
214
215     def call_retry(self):
216         self.timer = None
217         self.do_retry()
218         # Must be manually reset
219         return False
220
221     def block(self):
222         if not self.blocked:
223             global sus
224             self.blocked = True
225             sus.block()
226             log( "block by", self)
227             if self.blocking:
228                 b = self.blocking
229                 self.blocking = None
230                 b.release()
231     def unblock(self):
232         if self.blocked:
233             global sus
234             self.blocked = False
235             log( "unblock by", self)
236             sus.unblock()
237         if self.blocking:
238             b = self.blocking
239             self.blocking = None
240             b.release()
241
242 engines = []
243 def add_engine(e):
244     global engines
245     engines.append(e)
246
247 class state:
248     on = False
249     service = False
250     suspend = False
251 state = state()
252
253 def set_on(value):
254     global engines, state, sus
255     if state.on == value:
256         return
257     state.on = value
258     log("set on to", value)
259     if not value:
260         sus.block()
261     if not value:
262         for e in engines:
263             e.retry(False)
264             e.unblock()
265     for e in engines:
266         e.set_on(value)
267     if not value:
268         sus.unblock()
269     log("done setting on to", value)
270
271 def set_service(value):
272     global engines, state
273     if state.service == value:
274         return
275     state.service = value
276     for e in engines:
277         e.set_service(value)
278
279 def set_suspend(blocker):
280     global engines, state, sus
281     if state.suspend:
282         return
283     if sus.count:
284         suspend.abort_cycle()
285         return
286     state.suspend = True
287     for e in engines:
288         if e.blocking:
289             raise ValueError
290         blocker.block()
291         if e.set_suspend():
292             blocker.release()
293         else:
294             e.blocking = blocker
295
296 def set_resume():
297     global engines, state
298     if not state.suspend:
299         return
300     state.suspend = False
301     for e in engines:
302         e.set_resume()
303
304 watchers = {}
305 def watch(dir, base, handle):
306     global watchers
307     if not dir in watchers:
308         watchers[dir] = dnotify.dir(dir)
309     watchers[dir].watch(base, lambda x: gobject.idle_add(handle, x))
310
311
312 ###
313 # modem
314 #  manage a channel or two, allowing requests to be
315 #  queued and handling async notifications.
316 #  Each request has text to send, a reply handler, and
317 #  a timeout.  The reply handler indicates if more is expected.
318 #  Queued message might be marked 'ignore in suspend'.
319 #
320 # when told to switch on, raise the GPIO, open the devices
321 # send ATE0V1+CMEE=2;+CRC=1;+CMGF=0
322 # Then process queue.
323 # Every message that looks like it is async is handled as such
324 # and may have follow-ons.
325 # Every command should eventually be followed by OK or ERROR
326 # or +CM[ES] ERROR  or timeout.
327 # After a timeout we probe with 'AT' to get an 'OK'
328 # If no response and close/open doesn't work we rmmod ehci_omap and
329 # modprobe it again.
330 #
331 # When told to switch off, we drop the GPIO and queue a $QCPWRDN
332
333 # When an engine schedules an 'at' command, it either will get at
334 # least one callback, or will get 'set_on' to False, and then True
335
336
337 class CarrierDetect(AtChannel):
338     # on the hso modem in the GTA04, the 'NO CARRIER' signal
339     # arrives on the 'Modem' port, not on the 'Application' port.
340     # So we listen to the 'Modem' port, and report any
341     # 'NO CARRIER' we see - or indeed anything that we see.
342     def __init__(self, path, main):
343         AtChannel.__init__(self, path = path)
344         self.main = main
345
346     def takeline(self, line):
347         self.main.takeline(line)
348
349 class modem(Engine,AtChannel):
350     def __init__(self):
351         Engine.__init__(self)
352         AtChannel.__init__(self, "/dev/ttyHS_Application")
353         self.altchan = CarrierDetect("/dev/ttyHS_Modem", self)
354         self.queue = []
355         self.async = []
356         self.async_pending = None
357         self.pending_command = None
358         self.suspended = False
359         self.open_queued = False
360
361     def set_on(self, state):
362         if state:
363             self.open()
364         else:
365             self.queue = []
366             self.async_pending = None
367             self.pending_command = None
368             self.close()
369             gpio_set(186, 0)
370             time.sleep(0.1)
371             gpio_set(186,1)
372             time.sleep(0.5)
373             gpio_set(186,0)
374             time.sleep(2);
375             Popen("rmmod ehci_hcd", shell=True).wait();
376
377     def set_suspend(self):
378         self.suspended = True
379         if self.pending_command or self.async_pending:
380             log("Modem delays suspend")
381             return False
382         log("Modem allows suspend")
383         #self.close()
384         return True
385
386     def set_resume(self):
387         log("modem resumes")
388         self.suspended = False
389         #self.reopen()
390         self.cancel_timeout()
391         if self.connected:
392             self.pending_command = self.ignore
393             self.atcmd('')
394
395     def close(self):
396         self.disconnect()
397         self.cancel_timeout()
398         self.altchan.disconnect()
399
400     def open(self):
401         sleep_time=0.4
402         self.block()
403         gpio_set(186, 0)
404         self.close()
405         self.timedout()
406         while not self.connected:
407             time.sleep(sleep_time)
408             sleep_time *= 2
409             if self.connect(15):
410                 if self.altchan.connect(5):
411                     break
412             self.close()
413             if sleep_time > 30:
414                 print "will now reboot"
415                 sys.stdout.flush()
416                 Popen("/sbin/reboot -f", shell=True).wait()
417             Popen('rmmod ehci_omap; rmmod ehci-hcd; modprobe ehci-hcd; modprobe ehci_omap', shell=True).wait()
418             time.sleep(1)
419             gpio_set(186, 1)
420             time.sleep(0.5)
421             gpio_set(186, 0)
422             time.sleep(1)
423         l = self.wait_line(100)
424         while l != None:
425             l = self.wait_line(100)
426         self.pending_command = self.ignore
427         self.open_queued = False
428         self.atcmd('V1E0+CMEE=2;+CRC=1;+CMGF=0')
429
430     def reopen(self):
431         if not self.open_queued:
432             self.open_queued = True
433             gobject.idle_add(self.open)
434
435     def unblock(self):
436         if self.open_queued or self.pending_command or self.queue or self.async_pending:
437             print "cannot unblock:",self.open_queued, self.pending_command, self.queue, self.async_pending, self.suspended
438             return
439         print "modem unblock"
440         Engine.unblock(self)
441
442
443     def takeline(self, line):
444         if line == "":
445             # Just an extra '\r', ignore it.
446             return False
447         # Could be:
448         #  async message
449         #  async continuation
450         #  reply for recent command
451         #  final OK/ERR for recent command
452         #  error
453         if line == None:
454             self.reopen()
455             return False
456         if self.async_pending:
457             if self.async_pending(line):
458                 return False
459             self.async_pending = None
460         else:
461             if self.async_match(line):
462                 self.unblock()
463                 return self.async_pending == None
464             if self.pending_command:
465                 if not self.pending_command(line):
466                     self.pending_command = None
467                 if re.match('^(OK|\+CM[ES] ERROR|ERROR)', line):
468                     self.pending_command = None
469         if self.pending_command:
470             return False
471         gobject.idle_add(self.check_queue)
472         return True
473
474     def timedout(self):
475         if self.pending_command:
476             self.pending_command(None)
477             self.pending_command = None
478         if self.async_pending:
479             self.async_pending(None)
480             self.async_pending = None
481         self.pending_command = self.probe
482         if self.connected:
483             self.atcmd('', 10000)
484
485     def probe(self, line):
486         if line == "OK":
487             return False
488         # timeout
489         self.reopen()
490
491     def check_queue(self):
492         if not self.queue or self.pending_command or self.async_pending:
493             self.unblock()
494             return
495         if not self.connected:
496             return
497         if self.suspended:
498             return
499         cmd, cb, timeout = self.queue.pop()
500         if not cb:
501             cb = self.ignore
502         self.pending_command = cb
503         self.atcmd(cmd, timeout)
504
505     def ignore(self, line):
506         ## assume more to come until we see OK or ERROR
507         return True
508
509     def at_queue(self, cmd, handle, timeout):
510         self.block()
511         self.queue.append((cmd, handle, timeout))
512         gobject.idle_add(self.check_queue)
513
514     def clear_queue(self):
515         while self.queue:
516             cmd, cb, timeout = self.queue.pop()
517             if cb:
518                 cb(None)
519
520     def async_match(self, line):
521         for prefix, handle, extra in self.async:
522             if line[:len(prefix)] == prefix:
523                 # found
524                 if handle(line):
525                     self.async_pending = extra
526                     if extra:
527                         self.set_timeout(1000)
528                     return True
529         return False
530
531     def request_async(self, prefix, handle, extra):
532         self.async.append((prefix, handle, extra))
533
534
535 mdm = modem()
536 add_engine(mdm)
537 def request_async(prefix, handle, extras = None):
538     """ 'handle' should return True for a real match,
539     False if it was a false positive.
540     'extras' should return True if more is expected, or
541     False if there are no more async extras
542     """
543     global mdm
544     mdm.request_async(prefix, handle, extras)
545
546 def at_queue(cmd, handle, timeout = 5000):
547     global mdm
548     mdm.at_queue(cmd, handle, timeout)
549
550 ###
551 # flight
552 #  monitor the 'flightmode' file.  Powers the modem
553 #  on or off.  Reports off or on to all handlers
554 #  uses CFUN and PWRDN commands
555 class flight(Engine):
556     def __init__(self):
557         Engine.__init__(self)
558         watch('/var/lib/misc/flightmode','active', self.check)
559         gobject.idle_add(self.check)
560
561     def check(self, f = None):
562         self.block()
563         l = safe_read('/var/lib/misc/flightmode/active')
564         gobject.idle_add(self.turn_on, len(l) == 0)
565
566     def turn_on(self, state):
567         set_on(state)
568         self.unblock()
569
570     def set_suspend(self):
571         global state
572         l = safe_read('/var/lib/misc/flightmode/active')
573         if len(l) == 0 and not state.on:
574             self.block()
575             gobject.idle_add(self.turn_on, True)
576         elif len(l) > 0 and state.on:
577             self.block()
578             gobject.idle_add(self.turn_on, False)
579         return True
580
581 add_engine(flight())
582
583 ###
584 # register
585 #  transitions from 'on' to 'service' and reports
586 #  'no-service' when 'off' or no signal.
587 #  +CFUN=1  - turns on
588 #  +COPS=0  - auto select
589 #  +COPS=1,2,50502 - select specific (2 == numeric)
590 #  +COPS=3,1 - report format is long (1 == long)
591 #  +COPS=4,2,50502 - select specific with auto-fallback
592 #  http://www.shapeshifter.se/2008/04/30/list-of-at-commands/
593 class register(Engine):
594     def __init__(self):
595         Engine.__init__(self)
596         self.resuming_delay = 600
597
598     def set_on(self, state):
599         if state:
600             self.retry(0)
601         else:
602             set_service(False)
603
604     def do_retry(self):
605         at_queue('+CFUN?', self.gotline, 10000)
606
607     def wake_retry(self, handle):
608         log("Woke!")
609         global state
610         if state.on:
611             self.block()
612             at_queue('+CFUN?', self.got_wake_line, 8000)
613         return True
614
615     def got_wake_line(self, line):
616         log("CFUN wake for %s" % line)
617         self.gotline(line)
618         return False
619
620     def gotline(self, line):
621         if not line:
622             print "retry 1000 gotline not line"
623             self.retry(1000)
624             self.unblock()
625             return False
626         m = re.match('\+CFUN: (\d)', line)
627         if m:
628             n = m.group(1)
629             if n == '0' or n == '4':
630                 self.block()
631                 at_queue('+CFUN=1', self.did_set, 10000)
632                 return False
633             if n == '6':
634                 global mdm
635                 self.block()
636                 mdm.reopen()
637                 self.do_retry()
638             if n == '1':
639                 set_service(True)
640         print "retry end gotline"
641         self.retry()
642         self.unblock()
643         return False
644     def did_set(self, line):
645         print "retry 100 did_set"
646         self.retry(100)
647         self.unblock()
648         return False
649
650 reg = register()
651 add_engine(reg)
652 ###
653 # signal
654 #  While there is service, monitor signal strength.
655 class signal(Engine):
656     def __init__(self):
657         Engine.__init__(self)
658         request_async('_OSIGQ:', self.get_async)
659         self.delay = 120000
660         self.zero_count = 0
661
662     def set_service(self, state):
663         if state:
664             at_queue('_OSQI=1', None)
665         else:
666             record('signal_strength', '-/32')
667         self.retry()
668
669     def get_async(self, line):
670         m = re.match('_OSIGQ: ([0-9]+),([0-9]+)', line)
671         if m:
672             self.set(m.group(1))
673             return True
674         return False
675
676     def do_retry(self):
677         at_queue('+CSQ', self.get_csq)
678
679     def get_csq(self, line):
680         self.retry()
681         if not line:
682             return False
683         m = re.match('\+CSQ: ([0-9]+),([0-9]+)', line)
684         if m:
685             self.set(m.group(1))
686         return False
687     def set(self, strength):
688         record('signal_strength', '%s/32'%strength)
689         if strength == '0':
690             self.zero_count += 1
691             self.delay = 5000
692             global reg
693             reg.retry(0)
694         else:
695             self.zero_count = 0
696             self.delay = 120000
697         if self.zero_count > 10:
698             set_service(False)
699         self.retry()
700
701 add_engine(signal())
702
703 ###
704 # suspend
705 # There are three ways we interact with suspend
706 #  1/ we block suspend when something important is happening:
707 #    - any AT commands pending or active
708 #    - phone call active
709 #    - during initialisation?
710 #  2/ on suspend request we performs some checks before allowing the suspend,
711 #    and send notificaitons  on resume
712 #  3/ Some timeouts can wake up from suspend - e.g. CFUN poll.
713 #  When a suspend is pending, check call state and
714 #  sms gpio state and possibly abort.
715
716 class Blocker():
717     """initialise a counter to '1' and when it hits zero
718     call the callback
719     """
720     def __init__(self, cb):
721         self.cb = cb
722         self.count = 1
723     def block(self):
724         self.count += 1
725     def release(self):
726         self.count -= 1
727         if self.count == 0:
728             self.cb()
729             self.cb = None
730
731 class suspender(Engine):
732     def __init__(self):
733         Engine.__init__(self)
734         self.mon = suspend.monitor(self.want_suspend, self.resuming)
735
736     def want_suspend(self):
737         b = Blocker(lambda : self.mon.release())
738         set_suspend(b)
739         b.release()
740         return False
741
742     def resuming(self):
743         gobject.idle_add(set_resume)
744
745 add_engine(suspender())
746 ###
747 # location
748 #  when service, monitor cellid etc.
749 class Cellid(Engine):
750     def __init__(self):
751         Engine.__init__(self)
752         request_async('+CREG:', self.async)
753         request_async('+CBM:', self.cellname, extras=self.the_name)
754         self.last_try = 0
755         self.delay = 60000
756         self.newname = ''
757         self.cellnames = {}
758         self.lac = None
759
760     def set_on(self, state):
761         if state:
762             self.retry(100)
763
764     def set_resume(self):
765         # might have moved while we slept
766         if time.time() > self.last_try + 2*60:
767             self.block()
768             self.retry(0)
769
770     def set_service(self, state):
771         if not state:
772             record('cell', '-')
773             record('cellid','')
774             record('sid','')
775             record('carrier','-')
776
777     def do_retry(self):
778         self.last_try = time.time()
779         at_queue('+CREG?', self.got, 5000)
780         self.unblock()
781
782     def got(self, line):
783         self.retry()
784         if not line:
785             return False
786         if line[:9] == '+CREG: 0,':
787             at_queue('+CREG=2', None)
788         m = re.match('\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', line)
789         if m:
790             self.record(m)
791         return False
792
793     def async(self, line):
794         m = re.match('\+CREG: ([012])(,"([^"]*)","([^"]*)")?$', line)
795         if m:
796             if m.group(1) == '1' and m.group(2):
797                 self.record(m)
798                 self.retry()
799             if m.group(1) == '0':
800                 self.retry(0)
801             return True
802         return False
803
804     def cellname(self, line):
805         # get something like +CBM: 1568,50,1,1,1
806         # don't know what that means, just collect the 'extra' line
807         # I think the '50' means 'this is a cell id'.  I should
808         # probably test for that.
809         # Subsequent lines are the cell name.
810         m = re.match('\+CBM: \d+,\d+,\d+,\d+,\d+', line)
811         if m:
812             #ignore CBM content for now.
813             self.newname = ''
814             return True
815         return False
816
817     def the_name(self, line):
818         if not line:
819             if not self.newname:
820                 return
821             l = re.sub('[^!-~]+',' ', self.newname)
822             if self.cellid:
823                 self.names[self.cellid] = l
824             record('cell', l)
825             return False
826         if self.newname:
827             self.newname += ' '
828         self.newname += line
829         return True
830
831
832     def record(self, m):
833         if m.groups()[3] != None:
834             lac = int(m.group(3), 16)
835             cellid = int(m.group(4), 16)
836             record('cellid', '%04X %06X' % (lac, cellid))
837             self.cellid = cellid;
838             if cellid in self.cellnames:
839                 record('cell', self.cellnames[cellid])
840             if lac != self.lac:
841                 self.lac = lac
842                 # check we still have correct carrier
843                 at_queue('_OSIMOP', self.got_carrier)
844                 # Make sure we are getting async cell notifications
845                 at_queue('+CSCB=1', None)
846     def got_carrier(self, line):
847         #_OSIMOP: "YES OPTUS","YES OPTUS","50502"
848         if not line:
849             return False
850         m = re.match('_OSIMOP: "(.*)",".*","(.*)"', line)
851         if m:
852             record('sid', m.group(2))
853             record('carrier', m.group(1))
854         return False
855
856 add_engine(Cellid())
857
858
859 ###
860 # CIMI
861 #  get CIMI once per 'on'
862 class SIM_ID(Engine):
863     def __init__(self):
864         Engine.__init__(self)
865         self.CIMI = None
866         self.timeout = 2500
867
868     def set_on(self, state):
869         if state:
870             self.timeout = 2500
871             self.retry(100)
872         else:
873             self.CIMI = None
874             record('sim', '')
875
876     def got(self, line):
877         if line:
878             m = re.match('(\d\d\d+)', line)
879             if m:
880                 self.CIMI = m.group(1)
881                 record('sim', self.CIMI)
882                 self.retry(False)
883                 return False
884         if not self.CIMI:
885             self.retry(self.timeout)
886             if self.timeout < 60*60*1000:
887                 self.timeout += self.timeout
888         return False
889
890     def do_retry(self):
891         if not self.CIMI:
892             at_queue("+CIMI", self.got, 5000)
893
894 add_engine(SIM_ID())
895 ###
896 # protocol
897 #  monitor 2g/3g protocol and select preferred
898 # 0=only2g 1=only3g 2=prefer2g 3=prefer3g 4=staywhereyouare 5=auto
899 # _OPSYS=%d,2
900 class proto(Engine):
901     def __init__(self):
902         Engine.__init__(self)
903         self.confirmed = False
904         self.mode = 'x'
905         watch('/var/lib/misc/gsm','mode', self.update)
906
907     def update(self, f):
908         global state
909         self.set_service(state.service)
910
911     def set_service(self, state):
912         if not state:
913             self.confirmed = False
914         if state:
915             self.check()
916
917     def check(self):
918         l = safe_read("/var/lib/misc/gsm/mode", "3")
919         if len(l) and l[0] in "012345":
920             if self.mode != l[0]:
921                 self.mode = l[0]
922                 self.confirmed = False
923         self.do_retry()
924
925     def do_retry(self):
926         if self.confirmed:
927             return
928         at_queue("_OPSYS=%s,2" % self.mode, self.got,  5000)
929
930     def got(self, line):
931         if line == "OK":
932             self.confirmed = True;
933         self.retry()
934         return False
935
936 add_engine(proto())
937
938 ###
939 # data
940 # async _OWANCALL
941 # _OWANDATA _OWANCALL +CGDCONT
942 #
943 # if /run/gsm-state/data-APN contains an APN, make a data call
944 # else hangup any data.
945 # Data call involves
946 #    +CGDCONT=1,"IP","$APN"
947 #    _OWANCALL=1,1,0
948 # We then poll _OWANCALL?, get _OWANCALL: (\d), (\d)
949 # first number is '1' for us, second is
950 #     0 = Disconnected, 1 = Connected, 2 = In setup,  3 = Call setup failed.
951 # once connected, _OWANDATA? results in
952 #  _OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$'
953 #  IP DNS1 DNS2
954 # e.g.
955 #_OWANDATA: 1, 115.70.17.232, 0.0.0.0, 58.96.1.28, 220.233.0.4, 0.0.0.0, 0.0.0.0,144000
956 #  IP is stored in 'data' and used with 'ifconfig'
957 # DNS are stored in 'dns'
958 class data(Engine):
959     def __init__(self):
960         Engine.__init__(self)
961         self.apn = None
962         self.dns = None
963         self.ip = None
964         self.retry_state = ''
965         self.last_data_usage = None
966         self.last_data_time = time.time()
967         request_async('_OWANCALL:', self.call_status)
968         watch('/run/gsm-state', 'data-APN', self.check_apn)
969
970     def set_on(self, state):
971         if not state:
972             self.apn = None
973             self.hangup()
974
975     def set_service(self, state):
976         if state:
977             self.check_apn(None)
978         else:
979             self.hangup()
980
981     def check_apn(self, f):
982         global state
983         l = recall('data-APN')
984         if l == '':
985             l = None
986         if not state.service:
987             l = None
988
989         if self.apn != l:
990             self.apn = l
991             if self.ip:
992                 self.hangup()
993                 at_queue('_OWANCALL=1,0,0', None)
994
995             if self.apn:
996                 self.connect()
997
998     def hangup(self):
999         if self.ip:
1000             os.system('/sbin/ifconfig hso0 0.0.0.0 down')
1001         record('dns', '')
1002         record('data', '')
1003         self.ip = None
1004         self.dns = None
1005         if self.apn:
1006             self.retry_state = 'reconnect'
1007         else:
1008             self.retry_state = ''
1009         self.do_retry()
1010
1011     def connect(self):
1012         if not self.apn:
1013             return
1014         # reply to +CGDCONT isn't interesting, and reply to
1015         # _OWANCALL is handle by async handler.
1016         at_queue('+CGDCONT=1,"IP","%s"' % self.apn, None)
1017         at_queue('_OWANCALL=1,1,0', None)
1018         self.retry_state = 'calling'
1019         self.delay = 2000
1020         self.retry()
1021
1022     def do_retry(self):
1023         if self.retry_state == 'calling':
1024             at_queue('_OWANCALL?', None)
1025             return
1026         if self.retry_state == 'reconnect':
1027             self.connect()
1028         if self.retry_state == 'connected':
1029             self.check_connect()
1030
1031     def check_connect(self):
1032         self.retry_state = 'connected'
1033         self.delay = 60000
1034         self.retry()
1035         at_queue('_OWANDATA=1', self.connect_data)
1036
1037     def connect_data(self, line):
1038         m = re.match('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+, \d+$', line)
1039         if m:
1040             dns = (m.group(2), m.group(3))
1041             if self.dns != dns:
1042                 record('dns', '%s %s' % dns)
1043                 self.dns = dns
1044             ip = m.group(1)
1045             if self.ip != ip:
1046                 self.ip = ip
1047                 os.system('/sbin/ifconfig hso0 up %s' % ip)
1048                 record('data', ip)
1049             self.retry()
1050         if line == 'ERROR':
1051             self.hangup()
1052         return False
1053
1054     def call_status(self, line):
1055         m = re.match('_OWANCALL: (\d+), (\d+)', line)
1056         if not m:
1057             return False
1058         if m.group(1) != '1':
1059             return True
1060         s = int(m.group(2))
1061         if s == 0:
1062             # disconnected
1063             self.hangup()
1064         if s == 1:
1065             # connected
1066             self.check_connect()
1067         if s == 2:
1068             # in setup
1069             self.retry()
1070         if s == 3:
1071             # call setup failed
1072             self.apn = None
1073             self.hangup()
1074         return True
1075
1076     def log_update(self, force = False):
1077         global recording
1078         if 'sim' in recording and recording['sim']:
1079             sim = recording['sim']
1080         else:
1081             sim = 'unknown'
1082
1083         data_usage = self.last_data_usage
1084         data_time = self.last_data_time
1085         self.usage_update()
1086         if not data_usage or (not force and
1087                               self.last_data_time - data_time < 10 * 60):
1088             return
1089         call_log('gsm-data', '%s %d %d' % (
1090                 sim,
1091                 data_usage[0] - self.last_data_usage[0],
1092                 data_usage[1] - self.last_data_usage[1]))
1093
1094     def usage_update(self):
1095         self.last_data_usage = self.read_iface_usage()
1096         self.last_data_time = time.time()
1097         # data-last-usage allows app to add instantaneous current usage to
1098         # value from logs
1099         record('data-last-usage', '%s %s' % last_data_usage)
1100
1101
1102 add_engine(data())
1103
1104
1105 ###
1106 # config
1107 # Make sure CMGF CNMI etc all happen once per 'service'.
1108 # +CNMI=1,1,2,0,0 +CLIP?
1109 # +COPS
1110
1111 class config(Engine):
1112     def __init__(self):
1113         Engine.__init__(self)
1114     def set_service(self, state):
1115         if state:
1116             at_queue('+CLIP=1', None)
1117             at_queue('+CNMI=1,1,2,0,0', None)
1118
1119 add_engine(config())
1120
1121 ###
1122 # call
1123 #   ????
1124 # async +CRING RING +CLIP "NO CARRIER" "BUSY"
1125 # +CPAS
1126 # A   D
1127 # +VTS
1128 # +CHUP
1129 #
1130 # If we get '+CRING' or 'RING' we alert a call:
1131 #     record number to 'incoming', INCOMING to status and alert 'ring'
1132 #     and log the call
1133 # If we get +CLIP:, record and log the call detail
1134 # If we get 'NO CARRIER', clear 'status' and 'call'
1135 # If we get 'BUSY' clear 'call' and record status==BUSY
1136 #
1137 # Files to watch:
1138 #  'call' :might be 'answer', or a number or empty, to hang up
1139 #  'dtmf' : clear file and send DTMF tones
1140 #
1141 # Files we report:
1142 #  'incoming' is "-" for private, or "number" of incoming (or empty)
1143 #  'status' is "INCOMING" or 'BUSY' or 'on-call' (or empty)
1144 #
1145 # While 'on-call' we poll with +CLCC
1146 # ringing:
1147 #    +CLCC: 1,1,4,0,0,"0403463349",128
1148 # answered:
1149 #    +CLCC: 1,1,0,0,0,"0403463349",128
1150 # outgoing calling:
1151 #    +CLCC: 1,0,3,0,0,"0403463349",129
1152 # outgoing, got hangup
1153 #    +CLCC: 1,0,0,0,0,"0403463349",129
1154 #
1155 # Transitions are:
1156 #   Call :  idle -> active
1157 #   hangup: active,incoming,busy -> idle
1158 #   ring:   idle -> incoming
1159 #   answer: incoming -> active
1160 #   BUSY:   active -> busy
1161 #
1162 #
1163
1164 class voice(Engine):
1165     def __init__(self):
1166         Engine.__init__(self)
1167         self.state = 'idle'
1168         self.number = None
1169         self.zero_cnt = 0
1170         self.router = None
1171         request_async('+CRING', self.incoming)
1172         request_async('RING', self.incoming)
1173         request_async('+CLIP:', self.incoming_number)
1174         request_async('NO CARRIER', self.hangup)
1175         request_async('BUSY', self.busy)
1176         request_async('_OLCC', self.async_activity)
1177         watch('/run/gsm-state', 'call', self.check_call)
1178         watch('/run/gsm-state', 'dtmf', self.check_dtmf)
1179         self.f = EvDev('/dev/input/incoming', self.incoming_wake)
1180
1181     def set_on(self, state):
1182         record('call', '')
1183         record('dtmf', '')
1184         record('incoming', '')
1185         record('status', '')
1186
1187     def set_suspend(self):
1188         self.f.read(None, None)
1189         print "voice allows suspend"
1190         return True
1191
1192     def incoming_wake(self, dc, mom, typ, code, val):
1193         if typ == 1 and val == 1:
1194             self.block()
1195             self.zero_cnt = 0
1196             self.retry(0)
1197     def do_retry(self):
1198         at_queue('+CLCC', self.get_activity)
1199     def get_activity(self, line):
1200         m = re.match('\+CLCC: \d+,(\d+),(\d+),\d+,\d+,"([^"]*)".*', line)
1201         if m:
1202             n1 = m.group(1)
1203             n2 = m.group(2)
1204             num = m.group(3)
1205             if n1 == '1':
1206                 if n2 == '30':
1207                     self.to_idle()
1208                 if n2 == '4':
1209                     self.to_incoming(num)
1210             self.retry()
1211         else:
1212             self.to_idle()
1213         return False
1214
1215     def async_activity(self, line):
1216         m = re.match('_OLCC: \d+,(\d+),(\d+),\d+,\d+,"([^"]*)".*', line)
1217         if m:
1218             n1 = m.group(1)
1219             n2 = m.group(2)
1220             num = m.group(3)
1221             if n1 == '1':
1222                 if n2 == '30':
1223                     self.to_idle()
1224                 else:
1225                     self.to_incoming(num)
1226             return True
1227         return False
1228
1229     def incoming(self, line):
1230         self.to_incoming()
1231         return True
1232     def incoming_number(self, line):
1233         m = re.match('\+CLIP: "([^"]+)",[0-9,]*', line)
1234         if m:
1235             self.to_incoming(m.group(1))
1236         return True
1237     def hangup(self, line):
1238         self.to_idle()
1239         return True
1240     def busy(self, line):
1241         if self.state == 'active':
1242             record('status', 'BUSY')
1243         return True
1244
1245     def check_call(self, f):
1246         l = recall('call')
1247         if l == '':
1248             self.to_idle()
1249         elif l == 'answer':
1250             if self.state == 'incoming':
1251                 at_queue('A', self.answered)
1252                 set_alert('ring', None)
1253         elif self.state == 'idle':
1254             call_log('outgoing', l)
1255             at_queue('D%s;' % l, None)
1256             self.to_active()
1257
1258     def answered(self, line):
1259         self.to_active()
1260
1261     def check_dtmf(self, f):
1262         l = recall('dtmf')
1263         if l:
1264             record('dtmf', '')
1265         if self.state == 'active' and l:
1266             at_queue('+VTS=%s' % l, None)
1267
1268     def to_idle(self):
1269         if self.state == 'incoming' or self.state == 'active':
1270             call_log_end('incoming')
1271             call_log_end('outgoing')
1272         if self.state != 'idle':
1273             at_queue('+CHUP', None)
1274             record('incoming', '')
1275             record('status', '')
1276             if self.state == 'incoming':
1277                 num = 'Unknown Number'
1278                 if self.number:
1279                     num = self.number
1280                 sms = storesms.SMSmesg(source='MISSED-CALL', sender=num, text=('Missed call from %s' % self.number), state = 'NEW')
1281                 st = storesms.SMSstore(os.path.join(storesms.find_sms(),'SMS'))
1282                 st.store(sms)
1283             self.state = 'idle'
1284         if self.router:
1285             self.router.send_signal(15)
1286             self.router.wait()
1287             self.router = None
1288             try:
1289                 os.unlink('/run/sound/00-voicecall')
1290             except OSError:
1291                 pass
1292         self.number = None
1293         self.delay = 30000
1294         self.retry()
1295         self.unblock()
1296
1297     def to_incoming(self, number = None):
1298         self.block()
1299         n = '-call-'
1300         if number:
1301             n = number
1302         elif self.number:
1303             n = self.number
1304         if self.state != 'incoming' or (number and not self.number):
1305             call_log('incoming', n)
1306         if number:
1307             self.number = number
1308         record('incoming', n)
1309         record('status', 'INCOMING')
1310         set_alert('ring',n)
1311         self.delay = 500
1312         self.state = 'incoming'
1313         self.retry()
1314
1315     def to_active(self):
1316         self.block()
1317         if not self.router:
1318             try:
1319                 open('/run/sound/00-voicecall','w').close()
1320             except:
1321                 pass
1322             self.router = Popen('/usr/local/bin/gsm-voice-routing',
1323                                 close_fds = True)
1324         record('status', 'on-call')
1325         self.state = 'active'
1326         self.delay = 1500
1327         self.retry()
1328
1329 add_engine(voice())
1330
1331 ###
1332 # ussd
1333 # async +CUSD
1334
1335 ###
1336 # sms_recv
1337 # async +CMTI
1338 #
1339 # If we receive +CMTI, or a signal on /dev/input/incoming, then
1340 # we +CMGL=4 and collect messages an add them to the sms database
1341 class sms_recv(Engine):
1342     def __init__(self):
1343         Engine.__init__(self)
1344         self.check_needed = True
1345         request_async('+CMTI', self.must_check)
1346         self.f = EvDev("/dev/input/incoming", self.incoming)
1347         self.expecting_line = False
1348         self.messages = {}
1349         self.to_delete = []
1350
1351     def set_suspend(self):
1352         self.f.read(None, None)
1353         return True
1354
1355     def incoming(self, dc, mom, typ, code, val):
1356         if typ == 1 and val == 1:
1357             self.must_check('')
1358
1359     def must_check(self, line):
1360         self.check_needed = True
1361         self.retry(100)
1362         return True
1363
1364     def set_on(self, state):
1365         if state and self.check_needed:
1366             self.retry(100)
1367         if not state:
1368             self.unblock()
1369
1370     def do_retry(self):
1371         if not self.check_needed:
1372             if not self.to_delete:
1373                 return
1374             t = self.to_delete[0]
1375             self.to_delete = self.to_delete[1:]
1376             at_queue('+CMGD=%s' % t, self.did_delete)
1377             return
1378         global recording
1379         if 'sim' not in recording or not recording['sim']:
1380             self.retry(10000)
1381             return
1382         self.messages = {}
1383         # must not check when there is an incoming call
1384         at_queue('+CPAS', self.cpas)
1385
1386     def cpas(self, line):
1387         m = re.match('\+CPAS: (\d)', line)
1388         if m and m.group(1) == '3':
1389             self.retry(2000)
1390             return False
1391         self.block()
1392         at_queue('+CMGL=4', self.one_line, 40000)
1393         return False
1394
1395     def did_delete(self, line):
1396         if line != 'OK':
1397             return False
1398         self.retry(1)
1399         return False
1400
1401     def one_line(self, line):
1402         if line == 'OK':
1403             global recording
1404             self.check_needed = False
1405             found, res = storesms.sms_update(self.messages, recording['sim'])
1406             if res != None and len(res) > 10:
1407                 self.to_delete = res[:-10]
1408                 self.retry(1)
1409             if found:
1410                 set_alert('sms',reduce(lambda x,y: x+','+y, found))
1411             self.unblock()
1412             return False
1413         if not line or line[:5] == 'ERROR' or line[:10] == '+CMS ERROR':
1414             self.check_needed = True
1415             self.retry(60000)
1416             self.unblock()
1417             return False
1418         if self.expecting_line:
1419             self.expecting_line = False
1420             if self.designation != '0' and self.designation != '1':
1421                 # send, not recv !!
1422                 return True
1423             if len(line) < self.msg_len:
1424                 return True
1425             sender, date, ref, part, txt = sms.extract(line)
1426             self.messages[self.index] = (sender, date, txt, ref, part)
1427             return True
1428         m = re.match('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$', line)
1429         if m:
1430             self.expecting_line = True
1431             self.index = m.group(1)
1432             self.designation = m.group(2)
1433             self.msg_len = int(m.group(4), 10)
1434         return True
1435
1436
1437 add_engine(sms_recv())
1438
1439
1440 ###
1441 # sms_send
1442
1443
1444 c = gobject.main_context_default()
1445 while True:
1446     c.iteration()