summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author135e2 <[email protected]>2022-03-24 13:10:31 +0800
committer135e2 <[email protected]>2022-03-24 13:11:30 +0800
commit676d0507dca35367b7a2a320ec3194a3baa8830f (patch)
treedb07bfc93f93fc711b8a6829cfa08bcf2b00e3f2
parent7ff78c59eefe50fe5fdb5257244646176ec8313e (diff)
downloaddotfiles-676d0507dca35367b7a2a320ec3194a3baa8830f.tar.gz
dotfiles-676d0507dca35367b7a2a320ec3194a3baa8830f.tar.bz2
dotfiles-676d0507dca35367b7a2a320ec3194a3baa8830f.zip
scripts/tablet_mode: new, v1.0
- Downloaded from https://gist.github.com/ninlith/d0b56676c09b9d3142266c20c833d3da
-rw-r--r--scripts/tablet_mode251
1 files changed, 251 insertions, 0 deletions
diff --git a/scripts/tablet_mode b/scripts/tablet_mode
new file mode 100644
index 0000000..e159697
--- /dev/null
+++ b/scripts/tablet_mode
@@ -0,0 +1,251 @@
+#!/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() \ No newline at end of file