diff --git a/setup.sh b/setup.sh index ac18cdc..1904692 100755 --- a/setup.sh +++ b/setup.sh @@ -21,6 +21,11 @@ log '.: Setting up sono-os v0.1.0 :.' sleep 1 draw_progress_bar 5 +log 'Installing dependancies ...' +# TODO +sleep 1 + +draw_progress_bar 15 log 'Installing scripts ...' # TODO sleep 1 diff --git a/src/rules/90-drm.rules b/src/rules/90-drm.rules new file mode 100644 index 0000000..cbd4186 --- /dev/null +++ b/src/rules/90-drm.rules @@ -0,0 +1 @@ +SUBSYSTEM=="drm", ENV{MONITOR_LOCK}="/tmp/monitorlock", ENV{SONOLOG}="/tmp/sonolog.log", RUN+="/usr/local/bin/setupmonitor.sh" \ No newline at end of file diff --git a/src/scripts/python/conf/desktop.conf b/src/scripts/python/conf/desktop.conf new file mode 100644 index 0000000..11d68f1 --- /dev/null +++ b/src/scripts/python/conf/desktop.conf @@ -0,0 +1,3 @@ +[DEFAULT] +MainDisplay=HDMI-3 +Policy=Mirror diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index 3942701..7f71d69 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -29,15 +29,183 @@ Date: 2023 Mar 04 """ import Xlib.display +import Xlib.ext.randr +import configparser +from util import edit_distance +from util.egalax import get_egalax_drm_pure_name +import subprocess +from pathlib import Path +import os + +CONFIG_NAME = Path(os.path.dirname(os.path.realpath(__file__))) / "conf/desktop.conf" +XRANDR = "/usr/bin/xrandr" + +def read_config(): + """Reads config file of desktop setup + + This function will reeds the config file of desktop. Config file contins + a default monitor port name which will be the main Monitor display. + + NOTE: eGalax devide will be detected automatically from the /dev mountings. + """ + config = configparser.ConfigParser() + read = config.read(CONFIG_NAME) + if not read: + raise FileNotFoundError("Desktop config file not found") + return config + + +def all_connected_monitor(): + """Generates all connected monitors + + as a tuple of (atom name: str, width, height) + """ + display = Xlib.display.Display() + root = display.screen().root + for m in root.xrandr_get_monitors().monitors: + yield ( + display.get_atom_name(m.name), + m.width_in_pixels, + m.height_in_pixels, + ) + + +def get_edid_name(drm_name: str): + """Change eGalax DRM name to atom name + + This function is very sensitive to kernel version and might not work + with some kernels. + """ + card_num, name = drm_name[4:].split("-", maxsplit=1) + first, second = name.rsplit("-", maxsplit=1) + return first + "-" + card_num + "-" + second + + +def prepare_monitors(config): + """Prepare monitor names + + + Rules: + - Use default monitor port as the main monitor if it is connected + - If default monitor is not connected then use a monitor with + minimum edit distance. + + - Use eGalax as touchpanel and if it's not connected return None. + + - other connected monitors will be returned as a list + + each monitor is returen as a + + Returns + tuple: of + - main monitor -> tuple | None if no monitor available other than + touchpanel + - touchpanel -> tuple | None if touchpanel did not exist + - other -> list: this list may be empty if there is no other + monitor connected + """ + main = config["DEFAULT"]["MainDisplay"] + all_monitors = list(all_connected_monitor()) + egalax_drm = get_egalax_drm_pure_name() + egalax_name = get_edid_name(egalax_drm) if egalax_drm else None + egalax_monitor = None + main_monitor = None + for mon in all_monitors: + if egalax_name == mon[0]: + egalax_monitor = mon + if main == mon[0]: + main_monitor = mon + if egalax_monitor: + all_monitors.remove(egalax_monitor) + if not main_monitor: + try: + min_monitor = min( + all_monitors, + key=lambda x: edit_distance(main, x, len(main), len(x)), + ) + main_monitor = min_monitor + except: + main_monitor = None + assert len(all_monitors) == 0 + if main_monitor: + all_monitors.remove(main_monitor) + return main_monitor, egalax_monitor, all_monitors + + +def baseline(main, egalax): + """Base of xrandr arguments + + Both main and egalax are monitor tuples mentioned in prepare_monitors""" + if not main and not egalax: + return [], None + elif not main and egalax: + return ["--output", egalax[0], "--primary", + "--mode", f"{egalax[1]}x{egalax[2]}"], None + elif main and not egalax: + return ["--output", main[0], "--primary", + "--mode", f"{main[1]}x{main[2]}"], main[0] + else: + return ["--output", main[0], "--primary", + "--mode", f"{main[1]}x{main[2]}", + "--output", egalax[0], "--right-of", main[0], + "--mode", f"{egalax[1]}x{egalax[1]}"], egalax[0] + + +def mirror_displays(main, egalax, others: list): + base, should_mirror = baseline(main, egalax) + if should_mirror: + for name, width, height in others: + base.extend(["--output", name, "--mode", f"{width}x{height}", + "--same-as", main[0], + "--scale-from", f"{main[1]}x{main[2]}"]) + return base + + +def off_displays(main, egalax, others: list): + base, should_off = baseline(main, egalax) + if should_off: + for name, width, height in others: + base.extend(["--output", name, "--off"]) + return base + + +def stand_alone_displays(main, egalax, others: list): + base, rightmost = baseline(main, egalax) + if rightmost: + for name, width, height in others: + base.extend(["--output", name, "--mode", f"{width}x{height}", + "--right-of", rightmost]) + rightmost = name + return base + + +POLICY = { + 'Off': off_displays, + 'Mirror': mirror_displays, + 'StandAlone': stand_alone_displays +} + + +def config_xrandr(conf, main, egalax, others: list): + """Executes xrandr with policy in the conf + + Policies: + Policies are about monitors other than main and touch panel monitors. + There are three supported policies: + - Off: Disables other monitors (default policy if not in config) + - Mirror: Mirror other displays from main monitor. + - StandAlone: Each monitor is mapped to the right of each other + """ + try: + policy = conf['DEFAULT']['Policy'] + except: + policy = 'Off' + args = POLICY[policy](main, egalax, others) + cmd = [XRANDR] + args + subprocess.run(cmd) -display = Xlib.display.Display() -root = display.screen().root -for m in root.xrandr_get_monitors(True).monitors: - connector = display.get_atom_name(m.name) - print( - f"{connector}, {m.width_in_pixels}x{m.height_in_pixels}, " - f"x={m.x}, y={m.y}" - ) if __name__ == "__main__": - pass + conf = read_config() + main, egalax, others = prepare_monitors(conf) + config_xrandr(conf, main, egalax, others) diff --git a/src/scripts/python/util/__init__.py b/src/scripts/python/util/__init__.py new file mode 100644 index 0000000..7d9dd68 --- /dev/null +++ b/src/scripts/python/util/__init__.py @@ -0,0 +1 @@ +from .common import edit_distance, max_match diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/common.py index 6e2bd91..2ee76e4 100644 --- a/src/scripts/python/util/common.py +++ b/src/scripts/python/util/common.py @@ -33,3 +33,31 @@ def max_match(a: str, b: str) -> str: i += 1 else: return a[0:i] + + +def edit_distance(str1, str2, m, n): + # If first string is empty, the only option is to + # insert all characters of second string into first + if m == 0: + return n + + # If second string is empty, the only option is to + # remove all characters of first string + if n == 0: + return m + + # If last characters of two strings are same, nothing + # much to do. Ignore last characters and get count for + # remaining strings. + if str1[m - 1] == str2[n - 1]: + return edit_distance(str1, str2, m - 1, n - 1) + + # If last characters are not same, consider all three + # operations on last character of first string, recursively + # compute minimum cost for all three operations and take + # minimum of three values. + return 1 + min( + edit_distance(str1, str2, m, n - 1), # Insert + edit_distance(str1, str2, m - 1, n), # Remove + edit_distance(str1, str2, m - 1, n - 1), # Replace + ) diff --git a/src/scripts/python/util/egalax.py b/src/scripts/python/util/egalax.py index 1cf43aa..3056fb4 100644 --- a/src/scripts/python/util/egalax.py +++ b/src/scripts/python/util/egalax.py @@ -5,8 +5,8 @@ touchpannel's drm output and its overal status. """ from pathlib import Path -from x import get_edid_dev_path -from common import max_match +from .x import get_edid_dev_path +from .common import max_match VENDOR_ID = "0EEF" DEVICE_ID = "C000" diff --git a/src/scripts/setupmonitor.sh b/src/scripts/setupmonitor.sh new file mode 100755 index 0000000..40552d7 --- /dev/null +++ b/src/scripts/setupmonitor.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# This script will run when drm change event detected. +# This sctipt should be placed in /usr/local/bin +# SONOLOG file must be set beforehand in the udev rule +# MONITOR_LOCK should be set +( + flock -n 100 || exit 1 + sleep 1 # wait until all changes take place + xrandr --auto + python3 /usr/bin/local/python/setupmonitor.py + echo $(data) - INFO - Setup Monitor Done >> $SONOLOG +) 100> $MONITOR_LOCK \ No newline at end of file