Skip to content
Snippets Groups Projects

Asus C302 (CAVE) Orientation Daemon

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Max Regan

    Adapted from https://github.com/devendor/c302ca/blob/main/src/modewatcher.py, this version is slightly cleaner and also supports:

    • Suspending when the lid is closed without external monitors attached and without external power
    • Disabling the touchpad when the lid is closed to avoid phantom touches
    c302-tablet-mode-watcher.py 8.83 KiB
    #!/usr/bin/python3
    
    # Adapted from: https://github.com/devendor/c302ca/blob/main/src/modewatcher.py
    # Docs: https://github.com/devendor/c302ca#installing-modewatcher-with-onboard-on-screen-keyboard
    
    import json
    import subprocess
    import time
    import signal
    import logging
    import logging.handlers
    
    from dataclasses import dataclass
    from enum import Enum, auto
    
    logger = logging.getLogger("c302-tablet-mode-watcher")
    
    IIO_PATH_FMT = "/sys/bus/iio/devices/iio:device%s/in_accel_%s_raw"
    LID_STATE_PATH = "/proc/acpi/button/lid/LID0/state"
    TABLET_THRESHOLD = 300
    SLEEP_DELAY_SECS = 30 # Delay before sleeping to allow for quick open/close, or plugging in monitor after closing
    interval = .25
    stable_threshold = 2
    orientation_factor = 2
    orientation_threshold = 300
    
    V_KEYBOARD_NAME = "Virtual core keyboard"
    V_POINTER_NAME = "Virtual core pointer"
    KEYBOARD_NAME = "AT Translated Set 2 keyboard"
    TOUCHPAD_NAME  =  "Elan Touchpad"
    TOUCHSCREEN_NAME = "Elan Touchscreen"
    POWER_SUPPLY_NAME = "AC"
    
    LID_SENSOR_ID = 1
    BASE_SENSOR_ID = 2
    
    # State
    running = True
    
    @dataclass
    class Vector3d():
        x: float
        y: float
        z: float
    
    class DeviceModeEnum(Enum):
        CLOSED = auto()
        LAPTOP = auto()
        TENT = auto()
        KIOSK = auto()
        TABLET = auto()
    
        def __repr__(self):
            return self.name
    
    class DeviceOrientationEnum(Enum):
        NORMAL = auto()
        INVERTED = auto()
        LEFT = auto()
        RIGHT = auto()
    
        def __repr__(self):
            return self.name
    
    @dataclass
    class DeviceOrientation():
        mode: DeviceModeEnum
        orientation: DeviceOrientationEnum
        displays: int
        locked: bool
        powered: bool
    
    def run_cmd(cmd) -> str:
        return subprocess.check_output(cmd).decode()
    
    def get_connected_display_lines() -> list[str]:
        return list(
            filter(
                lambda l: " connected" in l,
                run_cmd(["xrandr"]).splitlines()
            )
        )
    
    def get_internal_display_name() -> str:
        # Sometimes the display is
        line = next(
            filter(
                # Matches displays like eDP1 and e-DP1
                lambda l: l.startswith("e"),
                get_connected_display_lines()
            )
        )
        return line.split(" ")[0]
    
    def get_on_ac_power() -> bool:
        return run_cmd(["acpi", "-a"]).split(":")[1].strip() == "on-line"
    
    
    def suspend_system():
        logger.info("suspending system")
        subprocess.run(["systemctl", "suspend", "-i"])
    
    def enable_touchpad():
        run_cmd(["/usr/bin/xinput", "reattach", TOUCHPAD_NAME, V_POINTER_NAME])
    
    def disable_touchpad():
        run_cmd(["/usr/bin/xinput", "float", TOUCHPAD_NAME])
    
    def enable_keyboard():
        run_cmd(["/usr/bin/xinput", "reattach", KEYBOARD_NAME, V_KEYBOARD_NAME])
    
    def disable_keyboard():
        run_cmd(["/usr/bin/xinput", "float", KEYBOARD_NAME])
    
    def enable_touchscreen():
        run_cmd(["/usr/bin/xinput", "reattach", TOUCHSCREEN_NAME, V_POINTER_NAME])
    
    def disable_touchscreen():
        run_cmd(["/usr/bin/xinput", "float", TOUCHSCREEN_NAME])
    
    
    def set_onscreen_keyboard_enabled(value):
        value = "true" if value == True else "false"
        subprocess.run(['/usr/bin/dbus-send','--type=method_call', '--dest=org.onboard.Onboard',
                        '/org/onboard/Onboard/Keyboard', 'org.freedesktop.DBus.Properties.Set',
                        'string:org.onboard.Onboard.Keyboard', 'string:AutoShowPaused', f"variant:boolean:{value}"])
    
    
    def get_accel(fp):
        fp.seek(0,0);
        return int(fp.readline().rstrip("\n"))
    
    def set_mode(orientation):
        display_name = get_internal_display_name()
        ORIENTATION_MAP = {
            DeviceOrientationEnum.NORMAL: "normal",
            DeviceOrientationEnum.INVERTED: "inverted",
            DeviceOrientationEnum.LEFT: "left",
            DeviceOrientationEnum.RIGHT: "right",
        }
    
        if orientation.mode == DeviceModeEnum.CLOSED:
            logger.info("Lid closed, disabling inputs")
            disable_touchscreen()
            disable_touchpad()
            disable_keyboard()
            # This is a big hack to delay going to sleep. Fortunately while the
            # lid is closed the orientation doesn't matter.
            start = time.time()
            while time.time() - start < SLEEP_DELAY_SECS:
                time.sleep(.25)
                orientation = get_current_orientation()
                first = True
                if orientation.displays > 1:
                    logger.info("Display attached, not sleeping")
                    return
                if orientation.mode != DeviceModeEnum.CLOSED:
                    logger.info("Lid opened, not sleeping")
                    return
                if orientation.powered:
                    logger.info("On AC power, not sleeping")
                    return
                if first:
                    first = False
                    logger.info(f"Waiting {SLEEP_DELAY_SECS} before sleeping")
            suspend_system()
            return
    
        enable_touchscreen()
        subprocess.run(["/usr/bin/xrandr", "--output", display_name, "--rotate", ORIENTATION_MAP[orientation.orientation]])
        subprocess.run(["/usr/bin/xinput", "--map-to-output", TOUCHSCREEN_NAME, display_name])
    
        if not orientation.locked:
            enable_touchpad()
            enable_keyboard()
            set_onscreen_keyboard_enabled(False)
        else:
            disable_touchpad()
            disable_keyboard()
            set_onscreen_keyboard_enabled(True)
    
    def get_lid_state() -> str:
        with open(LID_STATE_PATH, "r") as f:
            line = next(
                filter(
                    lambda l: l.startswith("state:"),
                    f
                )
            )
        # returns "open" or "closed"
        return line.split()[1]
    
    
    def get_iio_vector(ident: int) -> Vector3d:
        with open(IIO_PATH_FMT % (ident, 'x')) as iio_x, \
             open(IIO_PATH_FMT % (ident, 'y')) as iio_y, \
             open(IIO_PATH_FMT % (ident, 'z')) as iio_z:
            x = get_accel(iio_x)
            y = get_accel(iio_y)
            z = get_accel(iio_z)
        return Vector3d(x, y, z)
    
    def get_current_orientation() -> DeviceOrientation:
        base = get_iio_vector(BASE_SENSOR_ID)
        lid = get_iio_vector(LID_SENSOR_ID)
        lid_closed = True if get_lid_state() == "closed" else False
    
        mode = DeviceModeEnum.LAPTOP
        orientation = DeviceOrientationEnum.NORMAL
        locked = True
    
        if lid_closed == True:
            mode = DeviceModeEnum.CLOSED
            orientation = DeviceOrientationEnum.NORMAL
    
        elif (lid.y > 0) and (base.z > 0):
            mode = DeviceModeEnum.LAPTOP
            orientation = DeviceOrientationEnum.NORMAL
            locked = False
    
        elif abs(lid.z + base.z) < TABLET_THRESHOLD:
            mode = DeviceModeEnum.TABLET
            if (abs(lid.x) > abs(orientation_factor * lid.y) and
                abs(lid.x) > orientation_threshold
            ):
                orientation = DeviceOrientationEnum.RIGHT
            elif (abs(lid.y) > abs(orientation_factor * lid.x) and
                  abs(lid.y) > orientation_threshold
            ):
                orientation = DeviceOrientationEnum.LEFT
            else:
                orientation = DeviceOrientationEnum.RIGHT
    
        elif lid.y > 0:
            mode = DeviceModeEnum.KIOSK
            orientation = DeviceOrientationEnum.NORMAL
    
        else:
            mode = DeviceModeEnum.TENT
            orientation = DeviceOrientationEnum.INVERTED
    
        return DeviceOrientation(mode=mode,
                                 orientation=orientation,
                                 locked=locked,
                                 displays=len(get_connected_display_lines()),
                                 powered=get_on_ac_power())
    
    def daemon():
        global running
    
        logger.info("Starting mode watcher")
        current_set_orientation = None
        previous_orientation = None
        stable_reads = 0
    
        while running:
            current_orientation = get_current_orientation()
    
            if previous_orientation == None:
                set_mode(current_orientation)
                logger.info(f"Setting initial orientation: {current_orientation}")
                previous_orientation = current_orientation
                current_set_orientation = current_orientation
                stable_reads = 0
                continue
    
            if current_orientation == previous_orientation:
                stable_reads += 1
            else:
                previous_orientation = current_orientation
                stable_reads = 0
    
            # Short circuit "stability" checks for CLOSED, its based on a clean signal and minimizes spurious mouse movement when closing the lid
            if (current_orientation != current_set_orientation) and \
               ((current_orientation.mode == DeviceModeEnum.CLOSED) or (stable_reads > stable_threshold)):
                logger.info(f"Mode change: {current_orientation}")
                current_set_orientation = current_orientation
                set_mode(current_orientation)
            time.sleep(interval)
    
    def main():
        def do_exit(signal,frame):
            global running
            running = False
    
        logging.basicConfig(level=logging.INFO)
        logging.getLogger().addHandler(logging.handlers.SysLogHandler())
    
        signal.signal(signal.SIGHUP, do_exit)
        signal.signal(signal.SIGINT, do_exit)
        signal.signal(signal.SIGTERM, do_exit)
    
        daemon()
    
        logger.info("Signal received. Exiting")
    
    if __name__ == "__main__":
        main()
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment