]> git.neil.brown.name Git - plato.git/blob - gsm/gsmd2.py
gsmd2: modem: don't queue messages when disconnected.
[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         if not self.connected:
511             return
512         self.block()
513         self.queue.append((cmd, handle, timeout))
514         gobject.idle_add(self.check_queue)
515
516     def clear_queue(self):
517         while self.queue:
518             cmd, cb, timeout = self.queue.pop()
519             if cb:
520                 cb(None)
521
522     def async_match(self, line):
523         for prefix, handle, extra in self.async:
524             if line[:len(prefix)] == prefix:
525                 # found
526                 if handle(line):
527                     self.async_pending = extra
528                     if extra:
529                         self.set_timeout(1000)
530                     return True
531         return False
532
533     def request_async(self, prefix, handle, extra):
534         self.async.append((prefix, handle, extra))
535
536
537 mdm = modem()
538 add_engine(mdm)
539 def request_async(prefix, handle, extras = None):
540     """ 'handle' should return True for a real match,
541     False if it was a false positive.
542     'extras' should return True if more is expected, or
543     False if there are no more async extras
544     """
545     global mdm
546     mdm.request_async(prefix, handle, extras)
547
548 def at_queue(cmd, handle, timeout = 5000):
549     global mdm
550     mdm.at_queue(cmd, handle, timeout)
551
552 ###
553 # flight
554 #  monitor the 'flightmode' file.  Powers the modem
555 #  on or off.  Reports off or on to all handlers
556 #  uses CFUN and PWRDN commands
557 class flight(Engine):
558     def __init__(self):
559         Engine.__init__(self)
560         watch('/var/lib/misc/flightmode','active', self.check)
561         gobject.idle_add(self.check)
562
563     def check(self, f = None):
564         self.block()
565         l = safe_read('/var/lib/misc/flightmode/active')
566         gobject.idle_add(self.turn_on, len(l) == 0)
567
568     def turn_on(self, state):
569         set_on(state)
570         self.unblock()
571
572     def set_suspend(self):
573         global state
574         l = safe_read('/var/lib/misc/flightmode/active')
575         if len(l) == 0 and not state.on:
576             self.block()
577             gobject.idle_add(self.turn_on, True)
578         elif len(l) > 0 and state.on:
579             self.block()
580             gobject.idle_add(self.turn_on, False)
581         return True
582
583 add_engine(flight())
584
585 ###
586 # register
587 #  transitions from 'on' to 'service' and reports
588 #  'no-service' when 'off' or no signal.
589 #  +CFUN=1  - turns on
590 #  +COPS=0  - auto select
591 #  +COPS=1,2,50502 - select specific (2 == numeric)
592 #  +COPS=3,1 - report format is long (1 == long)
593 #  +COPS=4,2,50502 - select specific with auto-fallback
594 #  http://www.shapeshifter.se/2008/04/30/list-of-at-commands/
595 class register(Engine):
596     def __init__(self):
597         Engine.__init__(self)
598         self.resuming_delay = 600
599
600     def set_on(self, state):
601         if state:
602             self.retry(0)
603         else:
604             set_service(False)
605
606     def do_retry(self):
607         at_queue('+CFUN?', self.gotline, 10000)
608
609     def wake_retry(self, handle):
610         log("Woke!")
611         global state
612         if state.on:
613             self.block()
614             at_queue('+CFUN?', self.got_wake_line, 8000)
615         return True
616
617     def got_wake_line(self, line):
618         log("CFUN wake for %s" % line)
619         self.gotline(line)
620         return False
621
622     def gotline(self, line):
623         if not line:
624             print "retry 1000 gotline not line"
625             self.retry(1000)
626             self.unblock()
627             return False
628         m = re.match('\+CFUN: (\d)', line)
629         if m:
630             n = m.group(1)
631             if n == '0' or n == '4':
632                 self.block()
633                 at_queue('+CFUN=1', self.did_set, 10000)
634                 return False
635             if n == '6':
636                 global mdm
637                 self.block()
638                 mdm.reopen()
639                 self.do_retry()
640             if n == '1':
641                 set_service(True)
642         print "retry end gotline"
643         self.retry()
644         self.unblock()
645         return False
646     def did_set(self, line):
647         print "retry 100 did_set"
648         self.retry(100)
649         self.unblock()
650         return False
651
652 reg = register()
653 add_engine(reg)
654 ###
655 # signal
656 #  While there is service, monitor signal strength.
657 class signal(Engine):
658     def __init__(self):
659         Engine.__init__(self)
660         request_async('_OSIGQ:', self.get_async)
661         self.delay = 120000
662         self.zero_count = 0
663
664     def set_service(self, state):
665         if state:
666             at_queue('_OSQI=1', None)
667         else:
668             record('signal_strength', '-/32')
669         self.retry()
670
671     def get_async(self, line):
672         m = re.match('_OSIGQ: ([0-9]+),([0-9]+)', line)
673         if m:
674             self.set(m.group(1))
675             return True
676         return False
677
678     def do_retry(self):
679         at_queue('+CSQ', self.get_csq)
680
681     def get_csq(self, line):
682         self.retry()
683         if not line:
684             return False
685         m = re.match('\+CSQ: ([0-9]+),([0-9]+)', line)
686         if m:
687             self.set(m.group(1))
688         return False
689     def set(self, strength):
690         record('signal_strength', '%s/32'%strength)
691         if strength == '0':
692             self.zero_count += 1
693             self.delay = 5000
694             global reg
695             reg.retry(0)
696         else:
697             self.zero_count = 0
698             self.delay = 120000
699         if self.zero_count > 10:
700             set_service(False)
701         self.retry()
702
703 add_engine(signal())
704
705 ###
706 # suspend
707 # There are three ways we interact with suspend
708 #  1/ we block suspend when something important is happening:
709 #    - any AT commands pending or active
710 #    - phone call active
711 #    - during initialisation?
712 #  2/ on suspend request we performs some checks before allowing the suspend,
713 #    and send notificaitons  on resume
714 #  3/ Some timeouts can wake up from suspend - e.g. CFUN poll.
715 #  When a suspend is pending, check call state and
716 #  sms gpio state and possibly abort.
717
718 class Blocker():
719     """initialise a counter to '1' and when it hits zero
720     call the callback
721     """
722     def __init__(self, cb):
723         self.cb = cb
724         self.count = 1
725     def block(self):
726         self.count += 1
727     def release(self):
728         self.count -= 1
729         if self.count == 0:
730             self.cb()
731             self.cb = None
732
733 class suspender(Engine):
734     def __init__(self):
735         Engine.__init__(self)
736         self.mon = suspend.monitor(self.want_suspend, self.resuming)
737
738     def want_suspend(self):
739         b = Blocker(lambda : self.mon.release())
740         set_suspend(b)
741         b.release()
742         return False
743
744     def resuming(self):
745         gobject.idle_add(set_resume)
746
747 add_engine(suspender())
748 ###
749 # location
750 #  when service, monitor cellid etc.
751 class Cellid(Engine):
752     def __init__(self):
753         Engine.__init__(self)
754         request_async('+CREG:', self.async)
755         request_async('+CBM:', self.cellname, extras=self.the_name)
756         self.last_try = 0
757         self.delay = 60000
758         self.newname = ''
759         self.cellnames = {}
760         self.lac = None
761
762     def set_on(self, state):
763         if state:
764             self.retry(100)
765
766     def set_resume(self):
767         # might have moved while we slept
768         if time.time() > self.last_try + 2*60:
769             self.block()
770             self.retry(0)
771
772     def set_service(self, state):
773         if not state:
774             record('cell', '-')
775             record('cellid','')
776             record('sid','')
777             record('carrier','-')
778
779     def do_retry(self):
780         self.last_try = time.time()
781         at_queue('+CREG?', self.got, 5000)
782         self.unblock()
783
784     def got(self, line):
785         self.retry()
786         if not line:
787             return False
788         if line[:9] == '+CREG: 0,':
789             at_queue('+CREG=2', None)
790         m = re.match('\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', line)
791         if m:
792             self.record(m)
793         return False
794
795     def async(self, line):
796         m = re.match('\+CREG: ([012])(,"([^"]*)","([^"]*)")?$', line)
797         if m:
798             if m.group(1) == '1' and m.group(2):
799                 self.record(m)
800                 self.retry()
801             if m.group(1) == '0':
802                 self.retry(0)
803             return True
804         return False
805
806     def cellname(self, line):
807         # get something like +CBM: 1568,50,1,1,1
808         # don't know what that means, just collect the 'extra' line
809         # I think the '50' means 'this is a cell id'.  I should
810         # probably test for that.
811         # Subsequent lines are the cell name.
812         m = re.match('\+CBM: \d+,\d+,\d+,\d+,\d+', line)
813         if m:
814             #ignore CBM content for now.
815             self.newname = ''
816             return True
817         return False
818
819     def the_name(self, line):
820         if not line:
821             if not self.newname:
822                 return
823             l = re.sub('[^!-~]+',' ', self.newname)
824             if self.cellid:
825                 self.names[self.cellid] = l
826             record('cell', l)
827             return False
828         if self.newname:
829             self.newname += ' '
830         self.newname += line
831         return True
832
833
834     def record(self, m):
835         if m.groups()[3] != None:
836             lac = int(m.group(3), 16)
837             cellid = int(m.group(4), 16)
838             record('cellid', '%04X %06X' % (lac, cellid))
839             self.cellid = cellid;
840             if cellid in self.cellnames:
841                 record('cell', self.cellnames[cellid])
842             if lac != self.lac:
843                 self.lac = lac
844                 # check we still have correct carrier
845                 at_queue('_OSIMOP', self.got_carrier)
846                 # Make sure we are getting async cell notifications
847                 at_queue('+CSCB=1', None)
848     def got_carrier(self, line):
849         #_OSIMOP: "YES OPTUS","YES OPTUS","50502"
850         if not line:
851             return False
852         m = re.match('_OSIMOP: "(.*)",".*","(.*)"', line)
853         if m:
854             record('sid', m.group(2))
855             record('carrier', m.group(1))
856         return False
857
858 add_engine(Cellid())
859
860
861 ###
862 # CIMI
863 #  get CIMI once per 'on'
864 class SIM_ID(Engine):
865     def __init__(self):
866         Engine.__init__(self)
867         self.CIMI = None
868         self.timeout = 2500
869
870     def set_on(self, state):
871         if state:
872             self.timeout = 2500
873             self.retry(100)
874         else:
875             self.CIMI = None
876             record('sim', '')
877
878     def got(self, line):
879         if line:
880             m = re.match('(\d\d\d+)', line)
881             if m:
882                 self.CIMI = m.group(1)
883                 record('sim', self.CIMI)
884                 self.retry(False)
885                 return False
886         if not self.CIMI:
887             self.retry(self.timeout)
888             if self.timeout < 60*60*1000:
889                 self.timeout += self.timeout
890         return False
891
892     def do_retry(self):
893         if not self.CIMI:
894             at_queue("+CIMI", self.got, 5000)
895
896 add_engine(SIM_ID())
897 ###
898 # protocol
899 #  monitor 2g/3g protocol and select preferred
900 # 0=only2g 1=only3g 2=prefer2g 3=prefer3g 4=staywhereyouare 5=auto
901 # _OPSYS=%d,2
902 class proto(Engine):
903     def __init__(self):
904         Engine.__init__(self)
905         self.confirmed = False
906         self.mode = 'x'
907         watch('/var/lib/misc/gsm','mode', self.update)
908
909     def update(self, f):
910         global state
911         self.set_service(state.service)
912
913     def set_service(self, state):
914         if not state:
915             self.confirmed = False
916         if state:
917             self.check()
918
919     def check(self):
920         l = safe_read("/var/lib/misc/gsm/mode", "3")
921         if len(l) and l[0] in "012345":
922             if self.mode != l[0]:
923                 self.mode = l[0]
924                 self.confirmed = False
925         self.do_retry()
926
927     def do_retry(self):
928         if self.confirmed:
929             return
930         at_queue("_OPSYS=%s,2" % self.mode, self.got,  5000)
931
932     def got(self, line):
933         if line == "OK":
934             self.confirmed = True;
935         self.retry()
936         return False
937
938 add_engine(proto())
939
940 ###
941 # data
942 # async _OWANCALL
943 # _OWANDATA _OWANCALL +CGDCONT
944 #
945 # if /run/gsm-state/data-APN contains an APN, make a data call
946 # else hangup any data.
947 # Data call involves
948 #    +CGDCONT=1,"IP","$APN"
949 #    _OWANCALL=1,1,0
950 # We then poll _OWANCALL?, get _OWANCALL: (\d), (\d)
951 # first number is '1' for us, second is
952 #     0 = Disconnected, 1 = Connected, 2 = In setup,  3 = Call setup failed.
953 # once connected, _OWANDATA? results in
954 #  _OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$'
955 #  IP DNS1 DNS2
956 # e.g.
957 #_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
958 #  IP is stored in 'data' and used with 'ifconfig'
959 # DNS are stored in 'dns'
960 class data(Engine):
961     def __init__(self):
962         Engine.__init__(self)
963         self.apn = None
964         self.dns = None
965         self.ip = None
966         self.retry_state = ''
967         self.last_data_usage = None
968         self.last_data_time = time.time()
969         request_async('_OWANCALL:', self.call_status)
970         watch('/run/gsm-state', 'data-APN', self.check_apn)
971
972     def set_on(self, state):
973         if not state:
974             self.apn = None
975             self.hangup()
976
977     def set_service(self, state):
978         if state:
979             self.check_apn(None)
980         else:
981             self.hangup()
982
983     def check_apn(self, f):
984         global state
985         l = recall('data-APN')
986         if l == '':
987             l = None
988         if not state.service:
989             l = None
990
991         if self.apn != l:
992             self.apn = l
993             if self.ip:
994                 self.hangup()
995                 at_queue('_OWANCALL=1,0,0', None)
996
997             if self.apn:
998                 self.connect()
999
1000     def hangup(self):
1001         if self.ip:
1002             os.system('/sbin/ifconfig hso0 0.0.0.0 down')
1003         record('dns', '')
1004         record('data', '')
1005         self.ip = None
1006         self.dns = None
1007         if self.apn:
1008             self.retry_state = 'reconnect'
1009         else:
1010             self.retry_state = ''
1011         self.do_retry()
1012
1013     def connect(self):
1014         if not self.apn:
1015             return
1016         # reply to +CGDCONT isn't interesting, and reply to
1017         # _OWANCALL is handle by async handler.
1018         at_queue('+CGDCONT=1,"IP","%s"' % self.apn, None)
1019         at_queue('_OWANCALL=1,1,0', None)
1020         self.retry_state = 'calling'
1021         self.delay = 2000
1022         self.retry()
1023
1024     def do_retry(self):
1025         if self.retry_state == 'calling':
1026             at_queue('_OWANCALL?', None)
1027             return
1028         if self.retry_state == 'reconnect':
1029             self.connect()
1030         if self.retry_state == 'connected':
1031             self.check_connect()
1032
1033     def check_connect(self):
1034         self.retry_state = 'connected'
1035         self.delay = 60000
1036         self.retry()
1037         at_queue('_OWANDATA=1', self.connect_data)
1038
1039     def connect_data(self, line):
1040         m = re.match('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+, \d+$', line)
1041         if m:
1042             dns = (m.group(2), m.group(3))
1043             if self.dns != dns:
1044                 record('dns', '%s %s' % dns)
1045                 self.dns = dns
1046             ip = m.group(1)
1047             if self.ip != ip:
1048                 self.ip = ip
1049                 os.system('/sbin/ifconfig hso0 up %s' % ip)
1050                 record('data', ip)
1051             self.retry()
1052         if line == 'ERROR':
1053             self.hangup()
1054         return False
1055
1056     def call_status(self, line):
1057         m = re.match('_OWANCALL: (\d+), (\d+)', line)
1058         if not m:
1059             return False
1060         if m.group(1) != '1':
1061             return True
1062         s = int(m.group(2))
1063         if s == 0:
1064             # disconnected
1065             self.hangup()
1066         if s == 1:
1067             # connected
1068             self.check_connect()
1069         if s == 2:
1070             # in setup
1071             self.retry()
1072         if s == 3:
1073             # call setup failed
1074             self.apn = None
1075             self.hangup()
1076         return True
1077
1078     def log_update(self, force = False):
1079         global recording
1080         if 'sim' in recording and recording['sim']:
1081             sim = recording['sim']
1082         else:
1083             sim = 'unknown'
1084
1085         data_usage = self.last_data_usage
1086         data_time = self.last_data_time
1087         self.usage_update()
1088         if not data_usage or (not force and
1089                               self.last_data_time - data_time < 10 * 60):
1090             return
1091         call_log('gsm-data', '%s %d %d' % (
1092                 sim,
1093                 data_usage[0] - self.last_data_usage[0],
1094                 data_usage[1] - self.last_data_usage[1]))
1095
1096     def usage_update(self):
1097         self.last_data_usage = self.read_iface_usage()
1098         self.last_data_time = time.time()
1099         # data-last-usage allows app to add instantaneous current usage to
1100         # value from logs
1101         record('data-last-usage', '%s %s' % last_data_usage)
1102
1103
1104 add_engine(data())
1105
1106
1107 ###
1108 # config
1109 # Make sure CMGF CNMI etc all happen once per 'service'.
1110 # +CNMI=1,1,2,0,0 +CLIP?
1111 # +COPS
1112
1113 class config(Engine):
1114     def __init__(self):
1115         Engine.__init__(self)
1116     def set_service(self, state):
1117         if state:
1118             at_queue('+CLIP=1', None)
1119             at_queue('+CNMI=1,1,2,0,0', None)
1120
1121 add_engine(config())
1122
1123 ###
1124 # call
1125 #   ????
1126 # async +CRING RING +CLIP "NO CARRIER" "BUSY"
1127 # +CPAS
1128 # A   D
1129 # +VTS
1130 # +CHUP
1131 #
1132 # If we get '+CRING' or 'RING' we alert a call:
1133 #     record number to 'incoming', INCOMING to status and alert 'ring'
1134 #     and log the call
1135 # If we get +CLIP:, record and log the call detail
1136 # If we get 'NO CARRIER', clear 'status' and 'call'
1137 # If we get 'BUSY' clear 'call' and record status==BUSY
1138 #
1139 # Files to watch:
1140 #  'call' :might be 'answer', or a number or empty, to hang up
1141 #  'dtmf' : clear file and send DTMF tones
1142 #
1143 # Files we report:
1144 #  'incoming' is "-" for private, or "number" of incoming (or empty)
1145 #  'status' is "INCOMING" or 'BUSY' or 'on-call' (or empty)
1146 #
1147 # While 'on-call' we poll with +CLCC
1148 # ringing:
1149 #    +CLCC: 1,1,4,0,0,"0403463349",128
1150 # answered:
1151 #    +CLCC: 1,1,0,0,0,"0403463349",128
1152 # outgoing calling:
1153 #    +CLCC: 1,0,3,0,0,"0403463349",129
1154 # outgoing, got hangup
1155 #    +CLCC: 1,0,0,0,0,"0403463349",129
1156 #
1157 # Transitions are:
1158 #   Call :  idle -> active
1159 #   hangup: active,incoming,busy -> idle
1160 #   ring:   idle -> incoming
1161 #   answer: incoming -> active
1162 #   BUSY:   active -> busy
1163 #
1164 #
1165
1166 class voice(Engine):
1167     def __init__(self):
1168         Engine.__init__(self)
1169         self.state = 'idle'
1170         self.number = None
1171         self.zero_cnt = 0
1172         self.router = None
1173         request_async('+CRING', self.incoming)
1174         request_async('RING', self.incoming)
1175         request_async('+CLIP:', self.incoming_number)
1176         request_async('NO CARRIER', self.hangup)
1177         request_async('BUSY', self.busy)
1178         request_async('_OLCC', self.async_activity)
1179         watch('/run/gsm-state', 'call', self.check_call)
1180         watch('/run/gsm-state', 'dtmf', self.check_dtmf)
1181         self.f = EvDev('/dev/input/incoming', self.incoming_wake)
1182
1183     def set_on(self, state):
1184         record('call', '')
1185         record('dtmf', '')
1186         record('incoming', '')
1187         record('status', '')
1188
1189     def set_suspend(self):
1190         self.f.read(None, None)
1191         print "voice allows suspend"
1192         return True
1193
1194     def incoming_wake(self, dc, mom, typ, code, val):
1195         if typ == 1 and val == 1:
1196             self.block()
1197             self.zero_cnt = 0
1198             self.retry(0)
1199     def do_retry(self):
1200         at_queue('+CLCC', self.get_activity)
1201     def get_activity(self, line):
1202         m = re.match('\+CLCC: \d+,(\d+),(\d+),\d+,\d+,"([^"]*)".*', line)
1203         if m:
1204             n1 = m.group(1)
1205             n2 = m.group(2)
1206             num = m.group(3)
1207             if n1 == '1':
1208                 if n2 == '30':
1209                     self.to_idle()
1210                 if n2 == '4':
1211                     self.to_incoming(num)
1212             self.retry()
1213         else:
1214             self.to_idle()
1215         return False
1216
1217     def async_activity(self, line):
1218         m = re.match('_OLCC: \d+,(\d+),(\d+),\d+,\d+,"([^"]*)".*', line)
1219         if m:
1220             n1 = m.group(1)
1221             n2 = m.group(2)
1222             num = m.group(3)
1223             if n1 == '1':
1224                 if n2 == '30':
1225                     self.to_idle()
1226                 else:
1227                     self.to_incoming(num)
1228             return True
1229         return False
1230
1231     def incoming(self, line):
1232         self.to_incoming()
1233         return True
1234     def incoming_number(self, line):
1235         m = re.match('\+CLIP: "([^"]+)",[0-9,]*', line)
1236         if m:
1237             self.to_incoming(m.group(1))
1238         return True
1239     def hangup(self, line):
1240         self.to_idle()
1241         return True
1242     def busy(self, line):
1243         if self.state == 'active':
1244             record('status', 'BUSY')
1245         return True
1246
1247     def check_call(self, f):
1248         l = recall('call')
1249         if l == '':
1250             self.to_idle()
1251         elif l == 'answer':
1252             if self.state == 'incoming':
1253                 at_queue('A', self.answered)
1254                 set_alert('ring', None)
1255         elif self.state == 'idle':
1256             call_log('outgoing', l)
1257             at_queue('D%s;' % l, None)
1258             self.to_active()
1259
1260     def answered(self, line):
1261         self.to_active()
1262
1263     def check_dtmf(self, f):
1264         l = recall('dtmf')
1265         if l:
1266             record('dtmf', '')
1267         if self.state == 'active' and l:
1268             at_queue('+VTS=%s' % l, None)
1269
1270     def to_idle(self):
1271         if self.state == 'incoming' or self.state == 'active':
1272             call_log_end('incoming')
1273             call_log_end('outgoing')
1274         if self.state != 'idle':
1275             at_queue('+CHUP', None)
1276             record('incoming', '')
1277             record('status', '')
1278             if self.state == 'incoming':
1279                 num = 'Unknown Number'
1280                 if self.number:
1281                     num = self.number
1282                 sms = storesms.SMSmesg(source='MISSED-CALL', sender=num, text=('Missed call from %s' % self.number), state = 'NEW')
1283                 st = storesms.SMSstore(os.path.join(storesms.find_sms(),'SMS'))
1284                 st.store(sms)
1285             self.state = 'idle'
1286         if self.router:
1287             self.router.send_signal(15)
1288             self.router.wait()
1289             self.router = None
1290             try:
1291                 os.unlink('/run/sound/00-voicecall')
1292             except OSError:
1293                 pass
1294         self.number = None
1295         self.delay = 30000
1296         self.retry()
1297         self.unblock()
1298
1299     def to_incoming(self, number = None):
1300         self.block()
1301         n = '-call-'
1302         if number:
1303             n = number
1304         elif self.number:
1305             n = self.number
1306         if self.state != 'incoming' or (number and not self.number):
1307             call_log('incoming', n)
1308         if number:
1309             self.number = number
1310         record('incoming', n)
1311         record('status', 'INCOMING')
1312         set_alert('ring',n)
1313         self.delay = 500
1314         self.state = 'incoming'
1315         self.retry()
1316
1317     def to_active(self):
1318         self.block()
1319         if not self.router:
1320             try:
1321                 open('/run/sound/00-voicecall','w').close()
1322             except:
1323                 pass
1324             self.router = Popen('/usr/local/bin/gsm-voice-routing',
1325                                 close_fds = True)
1326         record('status', 'on-call')
1327         self.state = 'active'
1328         self.delay = 1500
1329         self.retry()
1330
1331 add_engine(voice())
1332
1333 ###
1334 # ussd
1335 # async +CUSD
1336
1337 ###
1338 # sms_recv
1339 # async +CMTI
1340 #
1341 # If we receive +CMTI, or a signal on /dev/input/incoming, then
1342 # we +CMGL=4 and collect messages an add them to the sms database
1343 class sms_recv(Engine):
1344     def __init__(self):
1345         Engine.__init__(self)
1346         self.check_needed = True
1347         request_async('+CMTI', self.must_check)
1348         self.f = EvDev("/dev/input/incoming", self.incoming)
1349         self.expecting_line = False
1350         self.messages = {}
1351         self.to_delete = []
1352
1353     def set_suspend(self):
1354         self.f.read(None, None)
1355         return True
1356
1357     def incoming(self, dc, mom, typ, code, val):
1358         if typ == 1 and val == 1:
1359             self.must_check('')
1360
1361     def must_check(self, line):
1362         self.check_needed = True
1363         self.retry(100)
1364         return True
1365
1366     def set_on(self, state):
1367         if state and self.check_needed:
1368             self.retry(100)
1369         if not state:
1370             self.unblock()
1371
1372     def do_retry(self):
1373         if not self.check_needed:
1374             if not self.to_delete:
1375                 return
1376             t = self.to_delete[0]
1377             self.to_delete = self.to_delete[1:]
1378             at_queue('+CMGD=%s' % t, self.did_delete)
1379             return
1380         global recording
1381         if 'sim' not in recording or not recording['sim']:
1382             self.retry(10000)
1383             return
1384         self.messages = {}
1385         # must not check when there is an incoming call
1386         at_queue('+CPAS', self.cpas)
1387
1388     def cpas(self, line):
1389         m = re.match('\+CPAS: (\d)', line)
1390         if m and m.group(1) == '3':
1391             self.retry(2000)
1392             return False
1393         self.block()
1394         at_queue('+CMGL=4', self.one_line, 40000)
1395         return False
1396
1397     def did_delete(self, line):
1398         if line != 'OK':
1399             return False
1400         self.retry(1)
1401         return False
1402
1403     def one_line(self, line):
1404         if line == 'OK':
1405             global recording
1406             self.check_needed = False
1407             found, res = storesms.sms_update(self.messages, recording['sim'])
1408             if res != None and len(res) > 10:
1409                 self.to_delete = res[:-10]
1410                 self.retry(1)
1411             if found:
1412                 set_alert('sms',reduce(lambda x,y: x+','+y, found))
1413             self.unblock()
1414             return False
1415         if not line or line[:5] == 'ERROR' or line[:10] == '+CMS ERROR':
1416             self.check_needed = True
1417             self.retry(60000)
1418             self.unblock()
1419             return False
1420         if self.expecting_line:
1421             self.expecting_line = False
1422             if self.designation != '0' and self.designation != '1':
1423                 # send, not recv !!
1424                 return True
1425             if len(line) < self.msg_len:
1426                 return True
1427             sender, date, ref, part, txt = sms.extract(line)
1428             self.messages[self.index] = (sender, date, txt, ref, part)
1429             return True
1430         m = re.match('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$', line)
1431         if m:
1432             self.expecting_line = True
1433             self.index = m.group(1)
1434             self.designation = m.group(2)
1435             self.msg_len = int(m.group(4), 10)
1436         return True
1437
1438
1439 add_engine(sms_recv())
1440
1441
1442 ###
1443 # sms_send
1444
1445
1446 c = gobject.main_context_default()
1447 while True:
1448     c.iteration()