Asus C302 (CAVE) Orientation Daemon
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
#!/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()
Please register or sign in to comment