Skip to content

Commit c439dfd

Browse files
committed
Initial commit
0 parents  commit c439dfd

10 files changed

+1149
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/__pycache__
2+
**/*.pyc

PPConfig.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#User editable config file. Change triggers here as you see fit.
2+
3+
import PPButtonMon, PPActions
4+
from PPTypes import *
5+
import time
6+
7+
XorgUser = 'ben' #User account Xorg is running on
8+
SoftLockResleepSecs = 5 #Shut the display off again after this amount of time if awoken by an event and NOT the user
9+
HardLockResleepSecs = 30 #After 30 seconds of a wakeup event, if the user hasn't unlocked us, go back to sleep
10+
11+
def CheckForSoftLock(ButtonStates): #Soft press power
12+
if any([ButtonStates[S].IsPressed for S in (ButtonType.VOLDOWN, ButtonType.VOLUP)]) \
13+
or not ButtonStates[ButtonType.POWER].IsPressed:
14+
return False #Probably a hard lock
15+
16+
if PPLockState.Instance.HardLocked or PPLockState.Instance.SoftLocked:
17+
PPActions.PerformUnlock()
18+
else:
19+
PPActions.PerformSoftLock()
20+
21+
return True
22+
23+
def CheckForHardLock(ButtonStates): #Vol down + power at same time
24+
if PPLockState.Instance.HardLocked:
25+
return False #Don't re-lock if we're already locked.
26+
27+
if not all([ButtonStates[S].IsPressed for S in (ButtonType.VOLDOWN, ButtonType.POWER)]):
28+
return False
29+
30+
PPActions.PerformHardLock()
31+
return True
32+
33+
def CheckForRotate(ButtonStates):
34+
if not all([not ButtonStates[S].IsPressed for S in ButtonStates]):
35+
return False #Can't have a button held down to trigger this
36+
37+
if not ButtonStates[ButtonType.VOLDOWN].ChangeTime or not ButtonStates[ButtonType.VOLUP].ChangeTime:
38+
return False
39+
40+
Distance = ButtonStates[ButtonType.VOLDOWN].ChangeTime - ButtonStates[ButtonType.VOLUP].ChangeTime
41+
42+
if Distance < 0:
43+
Distance = -Distance
44+
45+
46+
if Distance > 1500 or Distance > (time.time_ns() // 1000) + 1000: #No longer than 1.5secs distance, and not more than a second ago.
47+
return False
48+
49+
PPActions.PerformRotate()
50+
51+
return True
52+
53+
EventTriggerFuncs = (CheckForSoftLock, CheckForHardLock, CheckForRotate)

components/KernelControl.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import time
2+
3+
def SetCPUPower(CoreID, Online):
4+
with open(f'/sys/devices/system/cpu/cpu{CoreID}/online', 'w') as Desc:
5+
Desc.write(f'{int(Online)}')
6+
7+
def SetCPUPowersave(CoreID, UsePowersave):
8+
with open(f'/sys/devices/system/cpu/cpu{CoreID}/cpufreq/scaling_governor', 'w') as Desc:
9+
Desc.write('powersave' if UsePowersave else 'ondemand')
10+
11+
def ActivateSleep():
12+
with open('/sys/power/state', 'w') as Desc:
13+
Desc.write('mem')
14+
15+
time.sleep(0.25) #Give it a moment, though in my experience it's instant
16+
17+

components/PPActions.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
import KernelControl, XorgControl
3+
from PPTypes import PPLockState, ButtonAction
4+
import time
5+
6+
def PerformSoftLock():
7+
State = PPLockState.Instance
8+
9+
XorgControl.SetTouchscreenPower(False)
10+
11+
for CoreID in range(1, 4):
12+
KernelControl.SetCPUPower(CoreID, False)
13+
14+
KernelControl.SetCPUPowersave(0, True)
15+
16+
State.SoftLocked = True
17+
18+
print('Soft locked PinePhone')
19+
20+
def PerformHardLock():
21+
PerformSoftLock()
22+
23+
PPLockState.Instance.HardLocked = True
24+
25+
print('Performing hard lock')
26+
27+
KernelControl.ActivateSleep()
28+
29+
print('Woken from hard lock')
30+
31+
PPLockState.Instance.LastHardLockWake = int(time.time())
32+
33+
def PerformUnlock():
34+
State = PPLockState.Instance
35+
36+
State.HardLocked = False
37+
State.SoftLocked = False
38+
39+
for CoreID in range(4):
40+
KernelControl.SetCPUPower(CoreID, True)
41+
KernelControl.SetCPUPowersave(CoreID, False)
42+
43+
XorgControl.SetTouchscreenPower(True)
44+
45+
print('Unlocked PinePhone')
46+
47+
48+
def PerformRotate():
49+
if PPLockState.Instance.RightRotated:
50+
XorgControl.SetNormalRotation()
51+
print('Rotated to portrait orientation')
52+
else:
53+
XorgControl.SetRightRotation()
54+
print('Rotated to landscape orientation')
55+
56+
PPLockState.Instance.RightRotated = not PPLockState.Instance.RightRotated
57+
58+
ActionMap = {
59+
ButtonAction.UNLOCK : PerformUnlock,
60+
ButtonAction.SOFTLOCK : PerformSoftLock,
61+
ButtonAction.SCREENROTATE : PerformRotate,
62+
ButtonAction.HARDLOCK : PerformHardLock,
63+
}
64+

components/PPButtonMon.py

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import threading, struct, queue
2+
from PPTypes import *
3+
4+
FormatString = '@nnHHi'
5+
FormatSize = struct.calcsize(FormatString)
6+
7+
class KernelInputEventStruct:
8+
Fields = ('tv_sec', 'tv_usec', 'type', 'code', 'value')
9+
10+
def __init__(self, Input):
11+
Blob = struct.unpack(FormatString, Input)
12+
assert len(self.Fields) == len(Blob)
13+
14+
for Inc, Field in enumerate(self.Fields):
15+
setattr(self, Field, Blob[Inc])
16+
17+
@property
18+
def MSTime(self):
19+
return ((self.tv_sec * 1_000_000) + self.tv_usec) // 1000
20+
21+
class ButtonMonitor:
22+
def __init__(self):
23+
self.__PowerThread = threading.Thread(target=self.__PowerThreadFunc)
24+
self.__VolThread = threading.Thread(target=self.__VolThreadFunc)
25+
self.__ShouldDie = False
26+
self.__DieLock = threading.Lock()
27+
self.__ButtonStates = { ButtonType(S) : ButtonState(S) for S in (ButtonType.POWER, ButtonType.VOLDOWN, ButtonType.VOLUP) }
28+
self.__ButtonEvent = queue.Queue()
29+
self.__PowerThread.start()
30+
self.__VolThread.start()
31+
32+
@property
33+
def ShouldDie(self):
34+
with self.__DieLock:
35+
return bool(self.__ShouldDie)
36+
37+
@ShouldDie.setter
38+
def ShouldDie(self, Value):
39+
with self.__DieLock:
40+
self.__ShouldDie = Value
41+
42+
def WaitForChange(self, Timeout = None):
43+
try:
44+
return self.__ButtonEvent.get(timeout=Timeout)
45+
except queue.Empty:
46+
return None
47+
48+
def __del__(self):
49+
return #Might be blocking on a read(), just forget about it and let Python deal with it.
50+
51+
self.__ShouldDie = True
52+
self.__PowerThread.join()
53+
self.__VolThread.join()
54+
55+
def __VolThreadFunc(self):
56+
with open('/dev/input/event1', 'rb') as Desc:
57+
while not self.ShouldDie:
58+
Blob = Desc.read(FormatSize)
59+
60+
S = KernelInputEventStruct(Blob)
61+
62+
if not S.type or not S.code: continue #Filter useless data
63+
64+
print(f'Volume button event {S.__dict__}')
65+
66+
with self.__ButtonStates[ButtonType(S.code)] as Button:
67+
Button.IsPressed = bool(S.value)
68+
Button.LastChangeTime = Button.ChangeTime
69+
Button.ChangeTime = S.MSTime
70+
71+
self.__ButtonEvent.put(self.GetButtonStates())
72+
73+
74+
def __PowerThreadFunc(self):
75+
with open('/dev/input/event0', 'rb') as Desc:
76+
while not self.ShouldDie:
77+
Blob = Desc.read(FormatSize)
78+
79+
S = KernelInputEventStruct(Blob)
80+
81+
if not S.code: continue #Filter useless data
82+
83+
print(f'Power button event {S.__dict__}')
84+
85+
with self.__ButtonStates[ButtonType.POWER] as Button:
86+
Button.IsPressed = bool(S.value)
87+
Button.LastChangeTime = Button.ChangeTime
88+
Button.ChangeTime = S.MSTime
89+
90+
self.__ButtonEvent.put(self.GetButtonStates())
91+
92+
def GetButtonStates(self):
93+
return { S : self.__ButtonStates[S].Clone() for S in self.__ButtonStates }
94+

components/PPTypes.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import threading
2+
from enum import IntEnum, auto
3+
4+
class ButtonAction(IntEnum):
5+
INVALID = 0
6+
SOFTLOCK = 1
7+
HARDLOCK = 2
8+
UNLOCK = 3
9+
SCREENROTATE = 4
10+
POWERDOWN = 5
11+
MAX = auto()
12+
13+
class ButtonType(IntEnum):
14+
INVALID = 0
15+
POWER = 1
16+
VOLDOWN = 114
17+
VOLUP = 115
18+
MAX = auto()
19+
20+
class ButtonState:
21+
def __init__(self, Type):
22+
self.IsPressed = False
23+
self.ChangeTime = 0
24+
self.LastChangeTime = 0
25+
self.__Type = Type
26+
self.Lock = threading.Lock()
27+
28+
def __enter__(self):
29+
self.Lock.acquire()
30+
return self
31+
32+
def __exit__(self, *Discarded):
33+
self.Lock.release()
34+
35+
def Clone(self):
36+
B = ButtonState(self.Type)
37+
38+
with self.Lock:
39+
B.IsPressed = self.IsPressed
40+
B.ChangeTime = self.ChangeTime
41+
B.LastChangeTime = self.LastChangeTime
42+
43+
return B
44+
45+
@property
46+
def Type(self):
47+
return ButtonType(self.__Type)
48+
49+
class PPLockState:
50+
Instance = None
51+
52+
def __init__(self):
53+
if self.Instance:
54+
raise RuntimeError('Class already instantiated')
55+
56+
self.SoftLocked = False #Screen is off, touch is disabled, cores are in powersave
57+
self.HardLocked = False #We're in suspend/standby, waiting for a wake event
58+
self.RightRotated = False #Screen is rotated to the right
59+
self.LastHardLockWake = 0
60+
type(self).Instance = self

components/XorgControl.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import subprocess, re, os, sys, pwd, multiprocessing
2+
import PPConfig
3+
4+
def RunAsXorgUser(Username, Func, Args = tuple()):
5+
Recv, Send = multiprocessing.Pipe()
6+
7+
PID = os.fork()
8+
9+
if PID: #Parent process
10+
return Recv.recv()
11+
12+
User = pwd.getpwnam(Username)
13+
14+
os.setgid(User.pw_gid)
15+
os.setuid(User.pw_uid)
16+
17+
os.putenv('DISPLAY', ':0')
18+
19+
Send.send(Func(*Args))
20+
21+
os._exit(0) #Child terminates after it's done
22+
23+
24+
def _IsMonitorOnImpl():
25+
Stringy = subprocess.run(['xset', '-q'], capture_output=True).stdout.decode('utf-8')
26+
27+
return bool(re.match('.*Monitor is On.*', Stringy))
28+
29+
def IsMonitorOn():
30+
return RunAsXorgUser(PPConfig.XorgUser, _IsMonitorOnImpl)
31+
32+
def _FindTSIDImpl():
33+
Stringy = subprocess.run('xinput', capture_output=True).stdout.decode('utf-8')
34+
35+
Strings = Stringy.splitlines()
36+
37+
FoundString = None
38+
39+
Rx = re.compile('.*Goodix Capacitive TouchScreen.*id\=(\d+).*')
40+
41+
for Line in Strings:
42+
R = Rx.search(Line)
43+
44+
if not R:
45+
continue
46+
47+
try:
48+
if R.group(1).isnumeric():
49+
return R.group(1)
50+
51+
except:
52+
continue
53+
54+
def FindTSID():
55+
return RunAsXorgUser(PPConfig.XorgUser, _FindTSIDImpl)
56+
57+
58+
def __SetTouchscreenPowerImpl(Online):
59+
PowerString = 'on' if Online else 'off'
60+
TimeoutString = '0' if Online else '5'
61+
TouchString = '-enable' if Online else '-disable'
62+
63+
Cmds = (f'xset dpms force {PowerString}',
64+
f'xset dpms {TimeoutString}',
65+
f'xinput {TouchString} {FindTSID()}')
66+
67+
for Cmd in Cmds:
68+
os.system(Cmd)
69+
70+
def SetTouchscreenPower(Online):
71+
return RunAsXorgUser(PPConfig.XorgUser, __SetTouchscreenPowerImpl, (Online,))
72+
73+
def _SetRightRotationImpl():
74+
Cmds = ('xrandr --output DSI-1 --rotate right',
75+
f'xinput set-prop {FindTSID()} "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1')
76+
77+
for Cmd in Cmds:
78+
os.system(Cmd)
79+
80+
def SetRightRotation():
81+
return RunAsXorgUser(PPConfig.XorgUser, _SetRightRotationImpl)
82+
83+
def _SetNormalRotationImpl():
84+
Cmds = ('xrandr --output DSI-1 --rotate normal',
85+
f'xinput set-prop {FindTSID()} "Coordinate Transformation Matrix" 1 0 0 0 1 0 0 0 1')
86+
87+
for Cmd in Cmds:
88+
os.system(Cmd)
89+
90+
91+
def SetNormalRotation():
92+
return RunAsXorgUser(PPConfig.XorgUser, _SetNormalRotationImpl)

0 commit comments

Comments
 (0)