From 92d43f338124d75024fb26cfd0a3a8ac75809a1d Mon Sep 17 00:00:00 2001 From: NeilBrown Date: Sun, 6 Feb 2011 20:22:37 +1100 Subject: [PATCH] lock: various updates Signed-off-by: NeilBrown --- lock/alerts.doc | 61 ++++++ lock/lock-internalsound.py | 430 +++++++++++++++++++++++++++++++++++++ lock/lock.py | 281 +++++++++++++++++++++--- 3 files changed, 745 insertions(+), 27 deletions(-) create mode 100644 lock/alerts.doc create mode 100644 lock/lock-internalsound.py diff --git a/lock/alerts.doc b/lock/alerts.doc new file mode 100644 index 0000000..6061ddc --- /dev/null +++ b/lock/alerts.doc @@ -0,0 +1,61 @@ + +Alerts +------ + +'lock' is the intermediary for alerts. + +Alerts are registered by creating a file in /var/run/alert/NAME + +Providing 'lock' is running, it reads /etc/alert/NAME and performs the +actions listed. +These can be: + + sound:/file/name + vibrate:duration,on,off + led:name,on,off + flash: + +Alert sources are: + + SMS - this requires a single alert, no repeats. + file should be removed when alert ends. + RING - This alert can be actively repeated by changing the file, and + can be aborted by removing file. + + ALARM - once only, but can go for longer and get more annoying. + + LOW-BATTERY - single quiet short + +If the file still exists when the alert finishes we either + - replay the alert if the mtime has changed + - delete the file + +Other than 'sound' everything has duration, on, off. They are in millisecs +Duration defaults to the length of the sound, or 5 seconds if there is no sound +'on' defaults to 500 +'off' defaults to the same as 'on' + +names are: + everything in '/sys/class/leds' (unique substring works) + display + + +When the screen is woken up, everything stops. +If an alert is issued while the screen is awake, it continues until +some input. + +Different modes/personalities are selected by modes files created in +/var/lib/personality/XXX + +When reading the actions for an alert we read + alert + alert.XXX for each XXX + XXX for each XXX + +settings override, so if 'SILENT' contains 'sound:' then when SILENT mode +is in effect, no sound is made. + +So 'alert' serves as a default if no personality setting has been made. + +How does this allow for different tones depending on who the caller is? +Maybe the alert can be NAME.GROUP so SMS.friends diff --git a/lock/lock-internalsound.py b/lock/lock-internalsound.py new file mode 100644 index 0000000..3a16605 --- /dev/null +++ b/lock/lock-internalsound.py @@ -0,0 +1,430 @@ +#!/usr/bin/env python + +# This software is copyright Neil Brown 2009. +# It is licensed to you under the terms of the +# GNU General Public License version 2. +# +# +# blank and lock the screen when there is no activity. +# +# We detect activity by watching touch screen, power button, +# aux button +# After a period with no activity, we drop brightness to 20% +# and Grab all devices. +# Any activity at this stage returns us to 100% and no grab +# Continued inactivity turns display off. After that, require +# a button (AUX or Power) to reactivate. +# +# If the device seems really idle, we suspend with "apm -s" +# Other applications can disable this by getting a LOCK_SH +# lock on /var/run/suspend_disabled +# + +import gobject +import gtk +import fcntl +import os +import sys +import struct +import time +import suspend +import dnotify +import play +from subprocess import Popen, PIPE + +class EvDev: + def __init__(self, path, on_event): + self.f = os.open(path, os.O_RDWR|os.O_NONBLOCK); + self.ev = gobject.io_add_watch(self.f, gobject.IO_IN, self.read) + self.on_event = on_event + self.grabbed = False + self.downlist = [] + def read(self, x, y): + try: + str = os.read(self.f, 16) + except: + return True + + if len(str) != 16: + return True + (sec,usec,typ,code,value) = struct.unpack_from("IIHHI", str) + if typ == 0x01: + # KEY event + if value == 0: + # 'up' - remove from downlist + if code in self.downlist: + self.downlist.remove(code) + else: + # 'down' - add to downlist + if code not in self.downlist: + self.downlist.append(code) + self.on_event(self.down_count(), typ, code, value) + return True + def down_count(self): + if len(self.downlist) == 0: + return 0 + # double check, we might have missed an 'up' event somehow. + try: + rv = fcntl.ioctl(self.f, EVIOCGKEY(768/8), (768/8) * " " ) + l = len(rv)/4 + ra = struct.unpack('%dI' % l, rv) + isup = [] + for k in self.downlist: + by = int(k/8) + bt = k % 8 + if by < l and ((ra[by] >> bt) & 1) == 0: + isup.append[k] + for k in isup: + self.downlist.remove(k) + except: + pass + return len(self.downlist) + + def grab(self): + if self.grabbed: + return + #print "grab" + fcntl.ioctl(self.f, EVIOCGRAB, 1) + self.grabbed = True + def ungrab(self): + if not self.grabbed: + return + #print "release" + fcntl.ioctl(self.f, EVIOCGRAB, 0) + self.grabbed = False + +FBIOBLANK = 0x4611 +FB_BLANK_UNBLANK = 0 +FB_BLANK_POWERDOWN = 4 + +EVIOCGRAB = 0x40044590 +def EVIOCGKEY(len): + return 0x80004518 + len * 65536 + + +class SetAlerts: + def __init__(self, alertdir, actiondir): + # arrange to respond to alerts. + # If a name appears in 'alertdir', respond based + # on the content of the name name in 'actiondir'. + # Currently that must be a WAV file to be played + # If the file disappears, the action must stop instantly + # If the file is newer when the action completes, it is + # restarted + self.alertdir = alertdir + self.actiondir = actiondir + self.watch = dnotify.dir(alertdir) + self.active = {} + self.watch.watchall(self.runalert) + + def runalert(self): + gobject.idle_add(self.alert) + return True + + def alert(self): + print 'X' + # Only look for new entries here. + for n in os.listdir(self.alertdir): + if n in self.active: + continue + if n[0] == '.': + continue + self.add_alert(n) + print 'Y' + return False + + def add_alert(self, name): + print "adding", name + self.active[name] = (None, None, None) + w = self.watch.watch(name, lambda x : self.runcheck(x, name)) + if w.ino == 0: + del self.active[name] + # already disappeared + return + a = self.action(name) + self.active[name] = (w, a, w.mtime) + + def runcheck(self, w, name): + gobject.idle_add(lambda : self.check(w, name)) + return True + def check(self, w, name): + if name not in self.active: + print "check", name, "not found" + return False + print "check", name + (w2, a, mtime) = self.active[name] + if w.ino == 0: + if a: + a.finished = True + del self.active[name] + return False + if a == None and w.mtime > mtime + 1: + # play back had stopped - start it again + print "restart play" + a = self.action(name) + self.active[name] = (w, a, w.mtime) + return False + + def action(self, name, a = None): + try: + fname = os.path.join(self.actiondir, name) + if a: + a.setfile(fname) + else: + a = play.Play(fname, + done = lambda : self.action_done(name)) + except: + a = None + print "action", name, "retuned", a + return a + + def action_done(self, name): + print name, "done" + if name not in self.active: + return + (w, a, mtime) = self.active[name] + if w.ino == 0: + del self.active[name] + return + if w.mtime <= mtime + 3: + a = None + else: + print "play again", w.mtime, mtime + # play again + a = self.action(name, a) + self.active[name] = (w, a, w.mtime) + + + +class Screen: + def __init__(self): + self.state = "unknown" + f = open("/sys/class/backlight/gta02-bl/max_brightness") + self.max = int(f.read()) + f.close() + def bright(self, pcent): + b = int(pcent * self.max / 100) + f = open("/sys/class/backlight/gta02-bl/brightness","w") + f.write("%d\n" % b) + f.close() + def power(self, ioc): + f = open("/dev/fb0", "r+") + fcntl.ioctl(f, FBIOBLANK, ioc) + f.close() + def on(self): + if self.state != "on": + self.power(FB_BLANK_UNBLANK) + self.state = "on" + self.bright(100) + def dim(self): + if self.state != "on": + self.power(FB_BLANK_UNBLANK) + self.state = "on" + self.bright(20) + def off(self): + self.bright(0) + if self.state != "off": + self.power(FB_BLANK_POWERDOWN) + self.state = "off" + +def grab_all(): + global screen, power, aux + screen.grab() + power.grab() + aux.grab() + +def release_all(): + global screen, power, aux + screen.ungrab() + power.ungrab() + aux.ungrab() + +timeout = None +state = "full" +dimtime = 15 +enable_suspend = True +def set_timeout(): + global timeout + global state, enable_suspend + global dimtime + #print "set timeout for", state + if timeout != None: + gobject.source_remove(timeout) + if state == "full": + timeout = gobject.timeout_add(dimtime*1000, set_dim) + elif state == "dim": + timeout = gobject.timeout_add(10*1000, set_off) + elif state == "insta-lock": + timeout = gobject.timeout_add(1000, set_off) + elif state == "off" and enable_suspend: + timeout = gobject.timeout_add(30*1000, set_suspend) + + set_ico_file() + +def set_dim(): + global state + global aux, power, screen + if aux.down_count() + power.down_count() + screen.down_count() > 0: + # button still down + set_timeout() + return + display.dim() + grab_all() + state = "dim" + set_timeout() + +def set_off(): + global state, dimtime + display.off() + state = "off" + set_timeout() + dimtime = 15 + +def read_num(filename, default = 0, sep = None): + try: + f = open(filename) + except: + f = [ "%d" % default ] + num = default + for l in f: + l = l.split(sep)[0].strip() + try: + num = float(l) + break + except: + num = default + return num + +#Active Internet connections (w/o servers) +#Proto Recv-Q Send-Q Local Address Foreign Address State +#tcp 0 48 192.168.2.202:22 192.168.2.200:34244 ESTABLISHED +#tcp 0 0 192.168.2.202:22 192.168.2.200:41473 ESTABLISHED +def external_tcp(): + # check for TCP connections to external computers. + netstat = Popen(['netstat','-nt'], stdout = PIPE, close_fds = True) + for l in netstat.stdout: + l = l.strip() + f = l.split() + if f[0] != "tcp" or f[5] != 'ESTABLISHED': + continue + a = f[4].split(':') + if a[0] != "127.0.0.1": + return True + return False + +def set_suspend(): + global state, display + # Check for lock file + try: + f = open("/var/run/suspend_disabled", "r+") + except: + f = None + if f: + try: + fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) + except: + set_timeout() + return + + # Check for loadavg > 0.5 + load = read_num("/proc/loadavg") + if load > 0.5: + set_timeout() + return + + # Check for USB power, at least 500mA + current = read_num("/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim") + if current >= 500: + set_timeout() + return + + #if external_tcp(): + # set_timeout() + # return + + os.system("apm -s") + if f: + f.close() + rr = suspend.resume_reason() + if rr == 'PMU power-button-down': + wake_all(0) + return + + state = "off" + set_timeout() + + +def wake_all(down_cnt, *rest): + global state, dimtime + #print "wake all" + display.on() + if down_cnt == 0: + release_all() + if state == "dim" and dimtime < 120: + dimtime += 15 + if state != "disable": + state = "full" + set_timeout() + +def wake_dim(down_cnt, *rest): + global state + #print "wake_dim" + if state == "dim" or state == "full": + wake_all(down_cnt) + + +ico_file = None +def set_ico_file(): + global state, ico_file, ico + if state == "disable": + file = "lock-no.png" + elif state == "full": + if enable_suspend: + file = "lock-un.png" + else: + file = "lock-un-nosusp.png" + else: + if enable_suspend: + file = "lock.png" + else: + file = "lock-nosusp.png" + if file != ico_file: + ico.set_from_file("/usr/local/pixmaps/" + file) + ico_file = file + +last_ping = 0 +prev_ping = 0 +def ping(icon): + global state, enable_suspend, last_ping, prev_ping + if time.time() - prev_ping < 0.8: + grab_all() + state = "insta-lock" + elif state == "disable": + enable_suspend = False + state = "full" + elif not enable_suspend: + enable_suspend = True + state = "full" + else: + state = "disable" + prev_ping = last_ping + last_ping = time.time() + set_timeout() + +def main(): + global display, ico + global screen, power, aux + aux = EvDev("/dev/input/event4", wake_all) + power = EvDev("/dev/input/event0", wake_all) + screen = EvDev("/dev/input/event1", wake_dim) + alert = SetAlerts("/var/run/alert", "/etc/alert") + state = "full" + display = Screen() + display.on() + ico = gtk.StatusIcon() + set_timeout() + ico.connect("activate", ping) + + gtk.main() + +main() diff --git a/lock/lock.py b/lock/lock.py index 9adc658..42b22bc 100644 --- a/lock/lock.py +++ b/lock/lock.py @@ -27,6 +27,9 @@ import os import sys import struct import time +import suspend +import dnotify + from subprocess import Popen, PIPE class EvDev: @@ -44,7 +47,7 @@ class EvDev: if len(str) != 16: return True - (sec,usec,typ,code,value) = struct.unpack_from("IIHHI", str) + (sec,usec,typ,code,value) = struct.unpack_from("IIHHi", str) if typ == 0x01: # KEY event if value == 0: @@ -54,8 +57,11 @@ class EvDev: else: # 'down' - add to downlist if code not in self.downlist: - self.downlist.append(code) - self.on_event(self.down_count(), typ, code, value) + # ignore the 'play' butten (headphone insert) + if code != 207: + self.downlist.append(code) + moment = sec + float(usec)/1000000 + self.on_event(self.down_count(), moment, typ, code, value) return True def down_count(self): if len(self.downlist) == 0: @@ -98,6 +104,123 @@ EVIOCGRAB = 0x40044590 def EVIOCGKEY(len): return 0x80004518 + len * 65536 +def sysfs_write(file, val): + try: + f = open(file, "w") + f.write(val) + f.close() + except: + pass + +vib_timer = None +def set_vibrate(on, off, total): + global vib_timer + if vib_timer: + return + vib = "/sys/class/leds/neo1973:vibrator" + sysfs_write(vib+"/trigger", "none") + sysfs_write(vib+"/trigger", "timer") + sysfs_write(vib+"/delay_on", "%d"%on) + sysfs_write(vib+"/delay_off", "%d"%off) + vib_timer = gobject.timeout_add(total, clear_vibrator) +def clear_vibrator(): + global vib_timer + if vib_timer: + gobject.source_remove(vib_timer) + vib_timer = None + vib = "/sys/class/leds/neo1973:vibrator" + sysfs_write(vib+"/trigger", "none") + + +class SetAlerts: + def __init__(self, alertdir, actiondir): + # arrange to respond to alerts. + # If a name appears in 'alertdir', respond based + # on the content of the same name in 'actiondir'. + # Currently that must be a WAV file to be played + # If the file disappears, the action must stop instantly + # If the file is newer when the action completes, it is + # restarted + self.alertdir = alertdir + self.actiondir = actiondir + self.watch = dnotify.dir(alertdir) + self.active = {} + self.watch.watchall(self.runalert) + self.pref = "normal" + + def setpref(self, str): + self.pref = str + + def runalert(self): + gobject.idle_add(self.alert) + return True + + def alert(self): + # Only look for new entries here. + for n in os.listdir(self.alertdir): + if n in self.active: + continue + if n[0] == '.': + continue + self.add_alert(n) + return False + + def add_alert(self, name): + print "adding", name + self.active[name] = (None, None, None) + w = self.watch.watch(name, lambda x : self.runcheck(x, name)) + if w.ino == 0: + del self.active[name] + # already disappeared + return + a = self.action(name) + self.active[name] = (w, a, w.mtime) + + def runcheck(self, w, name): + gobject.idle_add(lambda : self.check(w, name)) + return True + def check(self, w, name): + if name not in self.active: + print "check", name, "not found" + return False + print "check", name + (w2, a, mtime) = self.active[name] + if w.ino == 0: + if a: + self.stop(a) + del self.active[name] + return False + #if a and not os.path.exists(a): + # a = None + #if a == None and w.mtime > mtime + 1: + # # play back had stopped - start it again + # print "restart play" + a = self.action(name) + self.active[name] = (w, a, w.mtime) + return False + + def action(self, name): + n = '/var/run/sound/14R-ring'+name + try: + os.symlink(os.path.join(self.actiondir, self.pref, name), n) + except: + pass + set_vibrate(200,400,1800) + if display.state != 'on': + set_dim() + return n + def stop(self, name): + try: + os.unlink(name) + except: + pass + + def stopall(self): + for n in self.active: + w, a, w.time = self.active[n] + self.stop(a) + self.active[n] = (w, None, 0) + class Screen: def __init__(self): self.state = "unknown" @@ -130,25 +253,53 @@ class Screen: self.state = "off" def grab_all(): - global screen, power, aux + global screen, power, aux, accel screen.grab() power.grab() aux.grab() + accel.grab() def release_all(): - global screen, power, aux + global screen, power, aux, accel screen.ungrab() power.ungrab() aux.ungrab() + accel.ungrab() + +# wall-clock might get reset when we wake from suspend, so +# use a monotonic clock to guard against early blanking. +import ctypes + +CLOCK_MONOTONIC = 1 # not sure about this one, check the headers + +class timespec(ctypes.Structure): + _fields_ = [ + ('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long) + ] + +librt = ctypes.CDLL('librt.so.1') +clock_gettime = librt.clock_gettime +clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)] + +def get_ticks(): + t = timespec() + clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(t)) + return t.tv_sec + t.tv_nsec / 1e9 + + timeout = None state = "full" dimtime = 15 enable_suspend = True +last_set = 0 def set_timeout(): global timeout global state, enable_suspend - global dimtime + global dimtime, last_set + + last_set = get_ticks() #print "set timeout for", state if timeout != None: gobject.source_remove(timeout) @@ -156,20 +307,22 @@ def set_timeout(): timeout = gobject.timeout_add(dimtime*1000, set_dim) elif state == "dim": timeout = gobject.timeout_add(10*1000, set_off) - elif state == "insta-lock": - timeout = gobject.timeout_add(1000, set_off) elif state == "off" and enable_suspend: timeout = gobject.timeout_add(30*1000, set_suspend) set_ico_file() def set_dim(): - global state + global state, last_set global aux, power, screen if aux.down_count() + power.down_count() + screen.down_count() > 0: # button still down set_timeout() return + if get_ticks() - last_set < dimtime - 1: + # if delay was too quick, try again + set_timeout() + return display.dim() grab_all() state = "dim" @@ -177,6 +330,10 @@ def set_dim(): def set_off(): global state, dimtime + global last_set + if get_ticks() - last_set < 9.5: + set_timeout() + return display.off() state = "off" set_timeout() @@ -218,7 +375,7 @@ def set_suspend(): global state, display # Check for lock file try: - f = open("/var/run/suspend_disabled", "r+") + f = open("/var/run/suspend_disabled", "w+") except: f = None if f: @@ -226,34 +383,38 @@ def set_suspend(): fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) except: set_timeout() + f.close() return # Check for loadavg > 0.5 - load = read_num("/proc/loadavg") - if load > 0.5: - set_timeout() - return + #load = read_num("/proc/loadavg") + #if load > 0.5: + # set_timeout() + # return # Check for USB power, at least 500mA + # If powered and network connection then leave awake. current = read_num("/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim") - if current >= 500: + if current >= 500 and external_tcp(): set_timeout() return - #if external_tcp(): - # set_timeout() - # return - os.system("apm -s") if f: f.close() - state = "full" + rr = suspend.resume_reason() + if rr == 'PMU power-button-down': + wake_all(0) + return + + state = "off" set_timeout() - display.on() + def wake_all(down_cnt, *rest): - global state, dimtime + global state, dimtime, alert #print "wake all" + alert.stopall() display.on() if down_cnt == 0: release_all() @@ -269,6 +430,63 @@ def wake_dim(down_cnt, *rest): if state == "dim" or state == "full": wake_all(down_cnt) +EV_SYN = 0 +EV_KEY = 1 +EV_ABS = 3 +ABS_X = 0 +ABS_Y = 1 +ABS_Z = 2 +BTN_X = 307 +BTN_Y = 308 +BTN_Z = 309 + +shake_cnt = 0 +shake_time = 0 +shake_seen = 0 +invert_timer = 0 +def check_attitude(down_cnt, moment, typ, code, value): + global state + global shake_cnt, shake_time, shake_seen + if moment - shake_time > 0.4: + shake_cnt = 0 + if typ == EV_ABS and abs(value) > 1500: + shake_time = moment + shake_seen = 1 + if typ == EV_SYN and shake_seen: + shake_cnt += 1 + shake_seen = 0 + if typ == EV_ABS and code == ABS_Y and value > 100: + shake_cnt = 0 + if shake_cnt >= 3: + shake_cnt = 0 + #no wake_all(0) + #no return + + if typ == EV_KEY and code <= BTN_Z and code >= BTN_X and value == 1: + wake_dim(0) + + global invert_timer + if typ == EV_ABS and code == ABS_Y and value > 500: + # upside down - need this for 0.6 seconds + #print "down", moment + if invert_timer == 0: + invert_timer = gobject.timeout_add(400, attitude_confirm) + elif typ == EV_ABS and code == ABS_Y: + #print "up", moment + if invert_timer: + gobject.source_remove(invert_timer) + invert_timer = 0 + +def attitude_confirm(): + global invert_timer, state + # seem to have been inverted for a while + invert_timer = 0 + if state != "off": + display.off() + grab_all() + state = "off" + set_timeout() + return False ico_file = None def set_ico_file(): @@ -293,10 +511,7 @@ last_ping = 0 prev_ping = 0 def ping(icon): global state, enable_suspend, last_ping, prev_ping - if time.time() - prev_ping < 0.8: - grab_all() - state = "insta-lock" - elif state == "disable": + if state == "disable": enable_suspend = False state = "full" elif not enable_suspend: @@ -308,12 +523,24 @@ def ping(icon): last_ping = time.time() set_timeout() +def setfile(name, val): + f = open(name, 'w') + f.write(val) + f.close() + def main(): global display, ico - global screen, power, aux + global screen, power, aux, accel + global alert aux = EvDev("/dev/input/event4", wake_all) power = EvDev("/dev/input/event0", wake_all) screen = EvDev("/dev/input/event1", wake_dim) + + alert = SetAlerts("/var/run/alert", "/etc/alert") + setfile('/sys/bus/spi/devices/spi3.1/threshold', '500') + setfile('/sys/bus/spi/devices/spi3.1/sample_rate','0') + setfile('/sys/bus/spi/devices/spi3.1/taps', '7000 5') + accel = EvDev("/dev/input/event3", check_attitude) state = "full" display = Screen() display.on() -- 2.43.0