]> git.neil.brown.name Git - plato.git/blob - utils/lock.py
a3aa82a477095a8b88321c148b1123c309911465
[plato.git] / utils / lock.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2009-2012 Neil Brown <neilb@suse.de>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License along
16 #    with this program; if not, write to the Free Software Foundation, Inc.,
17 #    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 #
19 #
20 # blank and lock the screen when there is no activity.
21 #
22 # We detect activity by watching touch screen, power button,
23 # aux button
24 # After a period with no activity, we drop brightness to 20%
25 # and Grab all devices.
26 # Any activity at this stage returns us to 100% and no grab
27 # Continued inactivity turns display off.  After that, require
28 # a button (AUX or Power) to reactivate.
29 #
30 # While display is lit we prevent suspend.  As soon as display
31 # is dark, suspend is allowed to progress.
32 #
33 # Also handle alerts.  Alerts are signaled by files appearing in
34 # /run/alert.  We respond by:
35 #  - disabling suspend
36 #  - triggering a vibrator rumble
37 #  - playing a suitable sound by linking a sound from /etc/alert/$name
38 #    to /run/sound.  A separate program plays the sound.
39 #  - restoring the display to "dim" if it is "off".
40 #
41
42 import gobject
43 import gtk
44 import fcntl
45 import os
46 import time
47 import suspend
48 import dnotify
49
50 from subprocess import Popen, PIPE
51 from vibra import Vibra
52 from evdev import EvDev
53
54 FBIOBLANK = 0x4611
55 FB_BLANK_UNBLANK = 0
56 FB_BLANK_POWERDOWN = 4
57
58 def sysfs_write(file, val):
59     try:
60         f = open(file, "w")
61         f.write(val)
62         f.close()
63     except:
64         pass
65
66 vib_timer = None
67 def gta02_set_vibrate(on, off, total):
68     global vib_timer
69     if vib_timer:
70         return
71     vib = "/sys/class/leds/neo1973:vibrator"
72     sysfs_write(vib+"/trigger", "none")
73     sysfs_write(vib+"/trigger", "timer")
74     sysfs_write(vib+"/delay_on", "%d"%on)
75     sysfs_write(vib+"/delay_off", "%d"%off)
76     vib_timer = gobject.timeout_add(total, clear_vibrator)
77 def clear_vibrator():
78     return
79     global vib_timer
80     if vib_timer:
81         gobject.source_remove(vib_timer)
82     vib_timer = None
83     vib = "/sys/class/leds/neo1973:vibrator"
84     sysfs_write(vib+"/trigger", "none")
85
86 vib_han = None
87 def set_vibrate(on, off, total):
88     global vib_han
89     if vib_han:
90         return
91     vib_han = Vibra()
92     vib_han.play(vib_han.multi_vibe(on, total/(on+off), off))
93     gobject.timeout_add(total, clear_vibe)
94 def clear_vibe():
95     global vib_han
96     if vib_han:
97         vib_han.close()
98         vib_han = None
99
100 class SetAlerts:
101     def __init__(self, alertdir, actiondir):
102         # arrange to respond to alerts.
103         # If a name appears in 'alertdir', respond based
104         # on the content of the same name in 'actiondir'.
105         # Currently that must be a WAV file to be played
106         # If the file disappears, the action must stop instantly
107         # If the file is newer when the action completes, it is
108         # restarted
109         self.alertdir = alertdir
110         self.actiondir = actiondir
111         self.watch = dnotify.dir(alertdir)
112         self.active = {}
113         self.watch.watchall(self.runalert)
114         self.pref = "normal"
115         self.blocker = suspend.blocker(False)
116
117     def setpref(self, str):
118         self.pref = str
119
120     def runalert(self):
121         self.blocker.block()
122         gobject.idle_add(self.alert)
123         return True
124
125     def alert(self):
126         # Only look for new entries here.
127         for n in os.listdir(self.alertdir):
128             if n in self.active:
129                 continue
130             if n[0] == '.':
131                 continue
132             self.add_alert(n)
133         self.blocker.unblock()
134         return False
135
136     def add_alert(self, name):
137         #print "adding", name
138         self.active[name] = (None, None, None)
139         w = self.watch.watch(name, lambda x : self.runcheck(x, name))
140         if w.ino == 0:
141             del self.active[name]
142             # already disappeared
143             return
144         a = self.action(name)
145         self.active[name] = (w, a, w.mtime)
146
147     def runcheck(self, w, name):
148         gobject.idle_add(lambda : self.check(w, name))
149         return True
150     def check(self, w, name):
151         if name not in self.active:
152             #print "check", name, "not found"
153             return False
154         #print "check", name
155         (w2, a, mtime) = self.active[name]
156         if w.ino == 0:
157             if a:
158                 self.stop(a)
159             del self.active[name]
160             return False
161         #if a and not os.path.exists(a):
162         #    a = None
163         #if a == None and w.mtime > mtime + 1:
164         #    # play back had stopped - start it again
165         #    print "restart play"
166         a = self.action(name)
167         self.active[name] = (w, a, w.mtime)
168         return False
169
170     def action(self, name):
171         n = '/run/sound/10-alert'
172         try:
173             os.symlink(os.path.join(self.actiondir, self.pref, name), n)
174         except:
175             pass
176         set_vibrate(200,400,1800)
177
178         if display.state != 'on':
179             set_dim()
180         suspend.abort_cycle()
181         return n
182     def stop(self, name):
183         try:
184             os.unlink(name)
185         except OSError:
186             pass
187
188     def stopall(self):
189         for n in self.active:
190             w, a, w_time = self.active[n]
191             if a:
192                 self.stop(a)
193             self.active[n] = (w, None, 0)
194
195 class Screen:
196     def __init__(self):
197         self.state = "unknown"
198         f = open("/sys/class/backlight/pwm-backlight/max_brightness")
199         self.max = int(f.read())
200         f.close()
201     def bright(self, pcent):
202         b = int(pcent * self.max / 100)
203         f = open("/sys/class/backlight/pwm-backlight/brightness","w")
204         f.write("%d\n" % b)
205         f.close()
206     def power(self, ioc):
207         f = open("/dev/fb0", "r+")
208         fcntl.ioctl(f, FBIOBLANK, ioc)
209         f.close()
210     def on(self):
211         if self.state != "on":
212             self.power(FB_BLANK_UNBLANK)
213             self.state = "on"
214         self.bright(100)
215
216     def dim(self):
217         if self.state != "on":
218             self.power(FB_BLANK_UNBLANK)
219             self.state = "on"
220         self.bright(20)
221     def off(self):
222         self.bright(0)
223         if self.state != "off":
224             self.power(FB_BLANK_POWERDOWN)
225             self.state = "off"
226
227 def grab_all():
228     global screen, power, aux, accel
229     screen.grab()
230     power.grab()
231     aux.grab()
232     #accel.grab()
233
234 def release_all():
235     global screen, power, aux, accel
236     screen.ungrab()
237     power.ungrab()
238     aux.ungrab()
239     #accel.ungrab()
240
241 # wall-clock might get reset when we wake from suspend, so
242 # use a monotonic clock to guard against early blanking.
243 import ctypes
244
245 CLOCK_MONOTONIC = 1 # not sure about this one, check the headers
246
247 class timespec(ctypes.Structure):
248     _fields_ = [
249         ('tv_sec', ctypes.c_long),
250         ('tv_nsec', ctypes.c_long)
251     ]
252
253 librt = ctypes.CDLL('librt.so.1')
254 clock_gettime = librt.clock_gettime
255 clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]
256
257 def get_ticks():
258     t = timespec()
259     clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(t))
260     return t.tv_sec + t.tv_nsec / 1e9
261
262 timeout = None
263 state = "full"
264 dimtime = 15
265 enable_suspend = True
266 last_set = 0
267 def set_timeout():
268     global timeout
269     global state
270     global dimtime, last_set
271
272     last_set = get_ticks()
273     if timeout != None:
274         gobject.source_remove(timeout)
275     if state == "full":
276         timeout = gobject.timeout_add(dimtime*1000, set_dim)
277     elif state == "dim":
278         timeout = gobject.timeout_add(10*1000, set_off)
279
280     set_ico_file()
281     global susblock, enable_suspend
282     if state == "off" and enable_suspend:
283         susblock.unblock()
284     else:
285         susblock.block()
286
287 def set_dim():
288     global state, last_set
289     global aux, power, screen
290     global one_down
291     if aux.down_count() + power.down_count() + screen.down_count() > 0:
292         # button still down
293         one_down = True
294         set_timeout()
295         return
296     one_down = False
297     if state != "off" and get_ticks() - last_set < dimtime - 1:
298         # if delay was too quick, try again
299         set_timeout()
300         return
301     display.dim()
302     grab_all()
303     state = "dim"
304     set_timeout()
305
306 def set_off():
307     global state, dimtime
308     global last_set
309     if get_ticks() - last_set < 9.5:
310         set_timeout()
311         return
312     grab_all()
313     display.off()
314     state = "off"
315     set_timeout()
316     dimtime = 15
317
318 def read_num(filename, default = 0, sep = None):
319     try:
320         f = open(filename)
321     except:
322         f = [ "%d" % default ]
323     num = default
324     for l in f:
325         l = l.split(sep)[0].strip()
326         try:
327             num = float(l)
328             break
329         except:
330             num = default
331     return num
332
333 #Active Internet connections (w/o servers)
334 #Proto Recv-Q Send-Q Local Address           Foreign Address         State
335 #tcp        0     48 192.168.2.202:22        192.168.2.200:34244     ESTABLISHED
336 #tcp        0      0 192.168.2.202:22        192.168.2.200:41473     ESTABLISHED
337 def external_tcp():
338     # check for TCP connections to external computers.
339     netstat = Popen(['netstat','-nt'], stdout = PIPE, close_fds = True)
340     for l in netstat.stdout:
341         l = l.strip()
342         f = l.split()
343         if f[0] != "tcp" or f[5] != 'ESTABLISHED':
344             continue
345         a = f[4].split(':')
346         if a[0] != "127.0.0.1":
347             return True
348     return False
349
350 def wake_all(down_cnt, moment, typ, code, value):
351     global state, dimtime, alert, suspended
352     if typ == 0:
353         # ignore sync events
354         return
355     if suspended:
356         return
357     if type == 1 and code != 330:
358         # EV_KEY but not BTN_TOUCH
359         alert.stopall()
360     display.on()
361     if down_cnt == 0:
362         release_all()
363     if state == "dim" and dimtime < 120:
364         dimtime += 15
365     if state != "disable":
366         state = "full"
367     set_timeout()
368
369 def wake_dim(down_cnt, moment, typ, code, value):
370     global state, suspended
371     if suspended:
372         return
373     if typ != 1: # EV_KEY
374         return
375     #print "wake_dim"
376     if state == "dim" or state == "full":
377         wake_all(down_cnt, moment, typ, code, value)
378
379 def wake_toggle(down_cnt, moment, typ, code, value):
380     global state, last_set, enable_suspend, suspended
381     if suspended:
382         return
383     # ignore down, just get up
384     if typ != 1 or value:
385         return
386     if ( state == "full" or state == "disable" ) and get_ticks() - last_set > 0.5:
387         enable_suspend = True
388         last_set = 0
389         set_off()
390     else:
391         wake_all(down_cnt, moment, typ, code, value)
392
393 EV_SYN = 0
394 EV_KEY = 1
395 EV_ABS = 3
396 ABS_X = 0
397 ABS_Y = 1
398 ABS_Z = 2
399 BTN_X = 307
400 BTN_Y = 308
401 BTN_Z = 309
402
403 shake_cnt = 0
404 shake_time = 0
405 shake_seen = 0
406 invert_timer = 0
407 def check_attitude(down_cnt, moment, typ, code, value):
408     global state
409     global shake_cnt, shake_time, shake_seen
410     if moment - shake_time > 0.4:
411         shake_cnt = 0
412     if typ == EV_ABS and abs(value) > 1500:
413         shake_time = moment
414         shake_seen = 1
415     if typ == EV_SYN and shake_seen:
416         shake_cnt += 1
417         shake_seen = 0
418     if typ == EV_ABS and code == ABS_Y and value > 100:
419         shake_cnt = 0
420     if shake_cnt >= 3:
421         shake_cnt = 0
422         #no  wake_all(0)
423         #no return
424
425     if typ == EV_KEY and code <= BTN_Z and code >= BTN_X and value == 1:
426         wake_dim(0)
427
428     global invert_timer
429     if typ == EV_ABS and code == ABS_Y and value > 500:
430         # upside down - need this for 0.6 seconds
431         #print "down", moment
432         if invert_timer == 0:
433             invert_timer = gobject.timeout_add(400, attitude_confirm)
434     elif typ == EV_ABS and code == ABS_Y:
435         #print "up", moment
436         if invert_timer:
437             gobject.source_remove(invert_timer)
438         invert_timer = 0
439
440 def attitude_confirm():
441     global invert_timer, state
442     # seem to have been inverted for a while
443     invert_timer = 0
444     if state != "off":
445         display.off()
446         grab_all()
447         state = "off"
448         set_timeout()
449     return False
450
451 ico_file = None
452 def set_ico_file():
453     global state, ico_file, ico
454     global aux, power, screen
455     down = aux.down_count() + power.down_count() + screen.down_count()
456     if state == "disable":
457         file = "lock-no.png"
458     elif state == "full":
459         if enable_suspend:
460             if down:
461                 file = "lock-un-pressed.png"
462             else:
463                 file = "lock-un.png"
464         else:
465             file = "lock-un-nosusp.png"
466     else:
467         if enable_suspend:
468             file = "lock.png"
469         else:
470             file = "lock-nosusp.png"
471     if file != ico_file:
472         ico.set_from_file("/usr/local/pixmaps/" + file)
473         ico_file = file
474
475 last_ping = 0
476 prev_ping = 0
477 def ping(icon):
478     global state, enable_suspend, last_ping, prev_ping
479     if state == "disable":
480         enable_suspend = False
481         state = "full"
482     elif not enable_suspend:
483         enable_suspend = True
484         state = "full"
485     else:
486         state = "disable"
487     prev_ping = last_ping
488     last_ping = time.time()
489     set_timeout()
490
491 def setfile(name, val):
492     f = open(name, 'w')
493     f.write(val)
494     f.close()
495
496 def do_release():
497     #print "Releasing"
498     global suspender
499     suspender.release()
500
501 def suspending():
502     # make sure all buttons have been checked
503     # everything  happens in events.  When we go idle,
504     # we have processed everything and can release the suspend.
505     global suspender, suspended
506     #print "suspending"
507     gobject.idle_add(do_release)
508     #suspended = True
509     return False
510
511 def resumed():
512     global suspended
513     #print "resumed"
514     suspended = False
515
516 def main():
517     global display, ico
518     global screen, power, aux, accel
519     global alert, suspender, susblock
520     aux = EvDev("/dev/input/aux", wake_all)
521     power = EvDev("/dev/input/power", wake_toggle)
522     screen = EvDev("/dev/input/touchscreen", wake_dim)
523
524     try:
525         os.mkdir("/run/alert")
526     except:
527         pass
528     alert = SetAlerts("/run/alert", "/etc/alert")
529     #setfile('/sys/bus/spi/devices/spi3.1/threshold', '500')
530     #setfile('/sys/bus/spi/devices/spi3.1/sample_rate','0')
531     #setfile('/sys/bus/spi/devices/spi3.1/taps', '7000 5')
532     #accel = EvDev("/dev/input/accel", check_attitude)
533     state = "full"
534     display = Screen()
535     display.on()
536     susblock = suspend.blocker()
537     ico = gtk.StatusIcon()
538     set_timeout()
539     ico.connect("activate", ping)
540
541     suspender.immediate(True)
542     gtk.main()
543
544 suspended = False
545 one_down = False
546 suspender = suspend.monitor(suspending, resumed)
547 main()