forked from funkypitt/Tinta4Plus-Universal
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResumeCheck.py
More file actions
330 lines (274 loc) · 12.6 KB
/
Copy pathResumeCheck.py
File metadata and controls
330 lines (274 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
"""
Post-resume / post-login display and input validation.
Checks that the display state is sane after suspend, hibernate, or login:
- Exactly one display is active (no extended-desktop bug)
- Active display has the correct resolution
- Display is positioned at (0,0), not offset
- Keyboard and pointer input devices are present
- Keyboard layout is restored
- Touchscreen is mapped to the active display
Can be used as a module (imported by the GUI) or run standalone.
"""
import os
import re
import subprocess
import time
class ResumeCheck:
"""Validate and fix display/input state after resume or login."""
# Expected hardware resolutions (native, before scaling)
OLED_RES = (2880, 1800)
EINK_RES = (2560, 1600)
def __init__(self, display_mgr, logger):
self.dm = display_mgr
self.logger = logger
# ------------------------------------------------------------------
# Main entry point
# ------------------------------------------------------------------
def run(self, expect_eink=False, saved_oled_scale=None,
saved_keyboard_layout=None):
"""Run all post-resume validation checks and apply fixes.
Args:
expect_eink: True if the eInk display should be active.
When unsure, pass False (default to OLED).
saved_oled_scale: Scale factor to restore on OLED (e.g. 1.75).
saved_keyboard_layout: Layout string to restore (from
DisplayManager.get_keyboard_layout()).
Returns:
list[str]: Human-readable log entries for each check/fix.
"""
results = []
# 1. Display state --------------------------------------------------
oled_on = self.dm.is_display_active(self.dm.OLED_CONNECTOR)
eink_on = self.dm.is_display_active(self.dm.EINK_CONNECTOR)
self.logger.info(
f"ResumeCheck: OLED={'on' if oled_on else 'off'}, "
f"eInk={'on' if eink_on else 'off'}, "
f"expect_eink={expect_eink}")
# --- both active (extended-desktop bug) ---
if oled_on and eink_on:
results.extend(self._fix_both_active(expect_eink,
saved_oled_scale))
# --- neither active ---
elif not oled_on and not eink_on:
results.extend(self._fix_none_active(expect_eink,
saved_oled_scale))
# --- wrong display active ---
elif expect_eink and oled_on and not eink_on:
# Wanted eInk but only OLED is on — unusual post-resume.
# Don't force eInk (needs USB T-CON), just warn.
results.append("Warning: expected eInk but only OLED is active")
elif not expect_eink and eink_on and not oled_on:
results.extend(self._switch_eink_to_oled(saved_oled_scale))
# 2. Geometry validation --------------------------------------------
results.extend(self._validate_geometry(expect_eink, saved_oled_scale))
# 3. Panning check (X11 only) --------------------------------------
results.extend(self._check_panning())
# 4. Keyboard layout -----------------------------------------------
if saved_keyboard_layout:
try:
self.dm.restore_keyboard_layout(saved_keyboard_layout)
results.append("Keyboard layout restored")
except Exception as e:
results.append(f"Warning: keyboard layout restore failed: {e}")
# 5. Input devices --------------------------------------------------
results.extend(self._check_input_devices())
# 6. Touch mapping --------------------------------------------------
active_connector = (self.dm.EINK_CONNECTOR if expect_eink
else self.dm.OLED_CONNECTOR)
try:
self.dm.map_touch_to_display(active_connector)
results.append(f"Touchscreen mapped to {active_connector}")
except Exception as e:
results.append(f"Warning: touch mapping failed: {e}")
for r in results:
self.logger.info(f"ResumeCheck: {r}")
return results
# ------------------------------------------------------------------
# Display fixes
# ------------------------------------------------------------------
def _fix_both_active(self, expect_eink, saved_oled_scale):
"""Both displays are on — disable the wrong one."""
fixes = []
if expect_eink:
self.dm.disable_display(self.dm.OLED_CONNECTOR)
fixes.append("Fixed: disabled OLED (both were active, eInk expected)")
else:
self.dm.disable_display(self.dm.EINK_CONNECTOR)
time.sleep(0.5)
scale = saved_oled_scale or 1.0
self.dm.enable_display(self.dm.OLED_CONNECTOR, scale=scale)
fixes.append("Fixed: disabled eInk and re-applied OLED "
"(both were active)")
return fixes
def _fix_none_active(self, expect_eink, saved_oled_scale):
"""No display is active — enable the expected one."""
fixes = []
if expect_eink:
self.dm.enable_display(self.dm.EINK_CONNECTOR)
fixes.append("Fixed: enabled eInk (no display was active)")
else:
scale = saved_oled_scale or 1.0
self.dm.enable_display(self.dm.OLED_CONNECTOR, scale=scale)
fixes.append("Fixed: enabled OLED (no display was active)")
return fixes
def _switch_eink_to_oled(self, saved_oled_scale):
"""eInk is active but OLED was expected — switch."""
self.dm.disable_display(self.dm.EINK_CONNECTOR)
time.sleep(0.5)
scale = saved_oled_scale or 1.0
self.dm.enable_display(self.dm.OLED_CONNECTOR, scale=scale)
return ["Fixed: switched from eInk to OLED"]
# ------------------------------------------------------------------
# Geometry validation
# ------------------------------------------------------------------
def _validate_geometry(self, expect_eink, saved_oled_scale):
"""Verify the active display has correct resolution and position."""
results = []
connector = (self.dm.EINK_CONNECTOR if expect_eink
else self.dm.OLED_CONNECTOR)
if not self.dm.is_display_active(connector):
return results
geom = self.dm.get_display_geometry(connector)
if not geom:
results.append(f"Warning: could not read geometry for {connector}")
return results
# Position check — should be at origin
if geom['x'] != 0 or geom['y'] != 0:
self.logger.warning(
f"ResumeCheck: {connector} at offset "
f"({geom['x']},{geom['y']}), repositioning to (0,0)")
scale = None if expect_eink else (saved_oled_scale or 1.0)
self.dm.enable_display(connector, scale=scale)
results.append(
f"Fixed: {connector} was at ({geom['x']},{geom['y']}), "
f"repositioned to (0,0)")
# Resolution check — compare against native resolution
# get_display_geometry returns logical (scaled) size, so we need to
# account for scaling when comparing
expected = self.EINK_RES if expect_eink else self.OLED_RES
scale = self.dm.get_display_scale(connector)
if scale and scale > 0:
logical_w = int(expected[0] / scale)
logical_h = int(expected[1] / scale)
else:
logical_w, logical_h = expected
# Allow small rounding tolerance
w_ok = abs(geom['width'] - logical_w) <= 2
h_ok = abs(geom['height'] - logical_h) <= 2
if not w_ok or not h_ok:
results.append(
f"Warning: {connector} resolution {geom['width']}x"
f"{geom['height']} differs from expected "
f"{logical_w}x{logical_h} (native {expected[0]}x{expected[1]}"
f" at scale {scale})")
return results
# ------------------------------------------------------------------
# Panning check (X11)
# ------------------------------------------------------------------
def _check_panning(self):
"""Detect xrandr panning (causes the scrolling-desktop bug)."""
if self.dm.session_type != 'x11':
return []
try:
result = subprocess.run(
['xrandr', '--query'], capture_output=True, text=True,
timeout=5)
if result.returncode != 0:
return []
issues = []
current_display = None
for line in result.stdout.splitlines():
# Match connected display lines
m = re.match(r'^(\S+)\s+connected', line)
if m:
current_display = m.group(1)
continue
# Look for panning info in mode lines
if current_display and 'panning' in line.lower():
issues.append(
f"Warning: {current_display} has panning enabled "
f"(may cause scrolling desktop)")
return issues
except Exception as e:
return [f"Warning: panning check failed: {e}"]
# ------------------------------------------------------------------
# Input device checks
# ------------------------------------------------------------------
def _check_input_devices(self):
"""Verify keyboard and pointer devices are present in the kernel."""
issues = []
try:
with open('/proc/bus/input/devices', 'r') as f:
content = f.read()
except Exception as e:
return [f"Warning: could not read input devices: {e}"]
has_keyboard = False
has_pointer = False
for block in content.split('\n\n'):
if not block.strip():
continue
# Extract handler line
handlers = ''
name = ''
for line in block.splitlines():
if line.startswith('H:'):
handlers = line.lower()
elif line.startswith('N:'):
name = line.lower()
if not handlers:
continue
# Keyboard: look for "kbd" handler with a real keyboard name
# (skip power buttons, video bus, etc.)
if 'kbd' in handlers and 'event' in handlers:
if ('keyboard' in name or 'at translated' in name or
'thinkpad' in name):
has_keyboard = True
# Pointer: mouse, touchpad, or pointing stick
if ('mouse' in handlers or
'touchpad' in name or 'trackpoint' in name or
'pointing stick' in name):
has_pointer = True
if not has_keyboard:
issues.append("Warning: no keyboard input device detected")
if not has_pointer:
issues.append("Warning: no pointer input device detected")
return issues
# ------------------------------------------------------------------
# Standalone entry point
# ------------------------------------------------------------------
def main():
"""Run resume checks standalone (for manual testing or systemd hook)."""
import logging
LOG_FILE = '/tmp/tinta4plusu.log'
logger = logging.getLogger('tinta4plusu-resume-check')
logger.setLevel(logging.INFO)
# Log to file (append) and console
fh = logging.FileHandler(LOG_FILE)
fh.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s %(message)s'))
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(levelname)s %(message)s'))
logger.addHandler(fh)
logger.addHandler(ch)
# Import DisplayManager from same directory
import sys
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
from DisplayManager import DisplayManager
dm = DisplayManager(logger)
checker = ResumeCheck(dm, logger)
# Determine expected state: if eDP-2 is the only active display,
# assume eInk was intended; otherwise default to OLED.
eink_on = dm.is_display_active(dm.EINK_CONNECTOR)
oled_on = dm.is_display_active(dm.OLED_CONNECTOR)
expect_eink = eink_on and not oled_on
logger.info(f"Standalone resume check: expect_eink={expect_eink}")
results = checker.run(expect_eink=expect_eink)
if results:
for r in results:
print(r)
else:
print("All checks passed, no issues found.")
if __name__ == '__main__':
main()