Last active
November 13, 2021 18:08
-
-
Save ninlith/d0b56676c09b9d3142266c20c833d3da to your computer and use it in GitHub Desktop.
Tablet mode (disable keyboard etc.) for a Crouton chroot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- indent-tabs-mode: nil; tab-width: 4 -*- | |
"""Enable/disable tablet mode in a Crouton chroot based on lid angle.""" | |
import argparse | |
import logging | |
import logging.config | |
import math | |
import os | |
import signal | |
import sys | |
import time | |
from collections import defaultdict | |
import numpy as np | |
logger = logging.getLogger(__name__) | |
def parse_command_line_args(): | |
"""Define and parse command-line options.""" | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
"-d", "--debug", | |
help="enable DEBUG logging level", | |
action="store_const", dest="loglevel", const=logging.DEBUG, | |
default=logging.INFO, | |
) | |
args = parser.parse_args() | |
return args | |
def setup_logging(loglevel): | |
"""Set up logging configuration.""" | |
logging_config = dict( | |
version=1, | |
disable_existing_loggers=False, | |
formatters={ | |
'f': { | |
'format': | |
"%(asctime)s %(levelname)s %(name)s - %(message)s", | |
'datefmt': "%F %T"}}, | |
handlers={ | |
'h': { | |
'class': "logging.StreamHandler", | |
'formatter': "f", | |
'level': loglevel}}, | |
root={ | |
'handlers': ["h"], | |
'level': loglevel}, | |
) | |
logging.config.dictConfig(logging_config) | |
class ConvertibleChromebook(object): | |
"""Convertible Chromebook.""" | |
def __init__(self, base_input_devices, touchscreen_device): | |
self.base_input_devices = base_input_devices | |
self.touchscreen_device = touchscreen_device | |
self.base_input_enabled = None | |
self.base_accel = [None, None, None] | |
self.lid_accel = [None, None, None] | |
self.lid_angle = None | |
self.previous_lid_angle = None | |
self.screen_orientation = "normal" | |
def read_accelerometers(self): | |
"""Get data from accelerometers.""" | |
command = ( | |
"grep --null '' /sys/class/chromeos/cros_ec/device" | |
"/cros-ec-accel.*/iio\:device*/* 2>/dev/null" | |
) | |
ret = os.popen(command).readlines() | |
paths_to_values = dict(line.rstrip().split('\0', 1) for line in ret) | |
tree = lambda: defaultdict(tree) | |
data = tree() | |
for path in paths_to_values: | |
dirname, filename = path.rsplit('/', 1) | |
data[dirname][filename] = paths_to_values[path] | |
for dirname in data: | |
location = data[dirname]['location'] | |
data[location] = data.pop(dirname) # Rename. | |
self.lid_accel = [x*float(data['lid']['scale']) for x in [ | |
float(data['lid']['in_accel_x_raw']), | |
float(data['lid']['in_accel_y_raw']), | |
float(data['lid']['in_accel_z_raw']), | |
]] | |
self.base_accel = [x*float(data['base']['scale']) for x in [ | |
float(data['base']['in_accel_x_raw']), | |
float(data['base']['in_accel_y_raw']), | |
float(data['base']['in_accel_z_raw']), | |
]] | |
def calculate_lid_angle(self): | |
""" | |
Calculate the lid angle based on the two accelerometers (base/lid). | |
When the lid angle is 180 degrees and the keyboard is on a horizontal | |
plane in front of an user, the standard orientation of both | |
accelerometers is: | |
+X axis is aligned with the hinge and pointing to the right. | |
+Y axis is in the same plane as the keyboard pointing towards the | |
top of the screen. | |
+Z axis is perpendicular to the keyboard, pointing out of the | |
keyboard. | |
This orientation is used in kernel 3.18 and later, previous kernel | |
might use different orientation. It's also used in Android and is | |
defined in the w3 spec: | |
http://www.w3.org/TR/orientation-event/#description. | |
""" | |
# https://chromium.googlesource.com/chromiumos/platform/factory/+/master/py/test/pytests/accelerometers_lid_angle.py | |
self.previous_lid_angle = self.lid_angle | |
hinge_vec = [9.8, 0.0, 0.0] # +X axis is aligned with the hinge. | |
base_vec_flattened = [0.0, self.base_accel[1], self.base_accel[2]] | |
lid_vec_flattened = [0.0, self.lid_accel[1], self.lid_accel[2]] | |
# http://en.wikipedia.org/wiki/Dot_product#Geometric_definition | |
# Use dot product and inverse cosine to get the angle between | |
# base_vec_flattened and lid_vec_flattened in degrees. | |
angle_between_vectors = math.degrees(math.acos( | |
np.dot(base_vec_flattened, lid_vec_flattened) / | |
np.linalg.norm(base_vec_flattened) / | |
np.linalg.norm(lid_vec_flattened))) | |
lid_angle = 180.0 - angle_between_vectors | |
# http://en.wikipedia.org/wiki/Cross_product#Geometric_meaning | |
# If the dot product of this cross product is normal, it means that the | |
# shortest angle between |base| and |lid| was counterclockwise with | |
# respect to the surface represented by |hinge| and this angle must be | |
# reversed. That means the current lid angle is >= 180 degrees and the | |
# value should be (360.0 - lid_angle), where lid_angle is always the | |
# smaller angle between the keyboard and the screen. | |
lid_base_cross_vec = np.cross(base_vec_flattened, lid_vec_flattened) | |
if np.dot(lid_base_cross_vec, hinge_vec) > 0.0: | |
self.lid_angle = 360.0 - lid_angle | |
else: | |
self.lid_angle = lid_angle | |
def disable_base_input(self): | |
"""Disable input devices located in the base.""" | |
if self.base_input_enabled is not False: | |
for input_device in self.base_input_devices: | |
os.system("xinput disable '{}'".format(input_device)) | |
self.base_input_enabled = False | |
def enable_base_input(self): | |
"""Enable input devices located in the base.""" | |
if self.base_input_enabled is not True: | |
for input_device in self.base_input_devices: | |
os.system("xinput enable '{}'".format(input_device)) | |
self.base_input_enabled = True | |
def orientate_screen(self, orientation=None, treshold=8.0, callback=None): | |
"""Change screen orientation.""" | |
if not orientation: | |
if self.lid_accel[1] > treshold: | |
orientation = "normal" | |
elif self.lid_accel[1] < -treshold: | |
orientation = "inverted" | |
elif self.lid_accel[0] < -treshold: | |
orientation = "left" | |
elif self.lid_accel[0] > treshold: | |
orientation = "right" | |
if orientation and orientation != self.screen_orientation: | |
logger.info("Setting screen orientation to '%s'...", orientation) | |
os.system("xrandr -o " + orientation) | |
matrices = { | |
"normal": "1 0 0 0 1 0 0 0 1", | |
"inverted": "-1 0 1 0 -1 1 0 0 1", | |
"left": "0 -1 1 1 0 0 0 0 1", | |
"right": "0 1 0 -1 0 1 0 0 1" | |
} | |
os.system( | |
"xinput set-prop '" + self.touchscreen_device + "' " | |
"'Coordinate Transformation Matrix' " + matrices[orientation]) | |
self.screen_orientation = orientation | |
callback(orientation) | |
def main(): | |
"""Main function.""" | |
def signal_handler(signum, frame): | |
"""Exit gracefully.""" | |
tablet_mode_exit() | |
sys.exit(0) | |
signal.signal(signal.SIGINT, signal_handler) | |
signal.signal(signal.SIGTERM, signal_handler) | |
args = parse_command_line_args() | |
setup_logging(args.loglevel) | |
cc = ConvertibleChromebook( | |
base_input_devices=["AT Translated Set 2 keyboard"], | |
touchscreen_device="Elan Touchscreen") | |
tablet_mode_enabled=False | |
def switch_xfce_panel_mode(orientation): | |
if orientation == "right" or orientation == "left": | |
os.system( | |
"xfconf-query -c xfce4-panel -p /panels/panel-1/mode -s 0") | |
else: | |
os.system( | |
"xfconf-query -c xfce4-panel -p /panels/panel-1/mode -s 1") | |
def tablet_mode_init(): | |
logger.info("Enabling tablet mode...") | |
cc.disable_base_input() | |
os.system("onboard &") | |
os.system("unclutter -root -idle 0.01 &") | |
def tablet_mode_exit(): | |
logger.info("Disabling tablet mode...") | |
cc.enable_base_input() | |
cc.orientate_screen("normal") | |
os.system("pkill onboard") | |
os.system("pkill unclutter") | |
while True: | |
cc.read_accelerometers() | |
logger.debug("Acceleration vectors (lid, base): %s, %s", | |
cc.lid_accel, cc.base_accel) | |
cc.calculate_lid_angle() | |
if cc.lid_angle < 20.00 and cc.previous_lid_angle > 180: | |
cc.lid_angle = 360.0 | |
logger.debug("Lid angle: %s", cc.lid_angle) | |
if cc.lid_angle > 180.0: | |
# Tablet mode. | |
if tablet_mode_enabled is not True: | |
tablet_mode_init() | |
tablet_mode_enabled = True | |
cc.orientate_screen(callback=switch_xfce_panel_mode) | |
elif abs(cc.lid_accel[0]) > 9.5: | |
# Lid angle calculation is unreliable when hinge aligns with | |
# gravity. | |
pass | |
else: | |
# Laptop mode. | |
if tablet_mode_enabled is not False: | |
tablet_mode_exit() | |
tablet_mode_enabled = False | |
time.sleep(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is there way to bring on screen keyboard when disabled ?_