#!/usr/bin/env python3 # -*- indent-tabs-mode: nil; tab-width: 4 -*- """ Enable/disable tablet mode in a Crouton chroot based on lid angle. Slightly modified from https://gist.github.com/ninlith/d0b56676c09b9d3142266c20c833d3da Author: ninlith & 135e2 Version: v1.2 """ 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__) ver = "v1.2" 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, ) parser.add_argument( "-V", "--version", help="show tablet_mode version", action="store_true" ) 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. Edit the path below for your chromebook. """ command = ( "grep --null '' /sys/class/chromeos/cros_ec/device/cros-ec-sensorhub.2.auto" "/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) orig_data = tree() data = {} # Create a new data dict instead. for path in paths_to_values: dirname, filename = path.rsplit("/", 1) orig_data[dirname][filename] = paths_to_values[path] for dirname in orig_data: location = orig_data[dirname]["location"] data[location] = orig_data[dirname] # Move data to the new dict. 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() if args.version: print("tablet_mode %s" % ver) sys.exit(0) 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 # Comment it out since we don't use xfce panel on i3. # 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()