From a821392ee4754f563a1dbf5108fa8b486438c0a3 Mon Sep 17 00:00:00 2001 From: tang1219 Date: Fri, 12 Jun 2026 16:54:32 +0800 Subject: [PATCH] V2-202509 --- .gitignore | 44 + DEBIAN/control | 8 + DEBIAN/postinst | 136 +++ DEBIAN/postrm | 70 ++ DEBIAN/preinst | 58 ++ DEBIAN/prerm | 5 + LICENSE | 18 + README.md | 3 + opt/ktouch/check_save | Bin 0 -> 20736 bytes opt/ktouch/desktops/screen-config.png | Bin 0 -> 376 bytes opt/ktouch/desktops/screen-remap.png | Bin 0 -> 345 bytes opt/ktouch/desktops/touchscreen-auto.desktop | 10 + .../desktops/touchscreen-config.desktop | 10 + opt/ktouch/desktops/touchscreen-now.desktop | 10 + opt/ktouch/display_monitor.py | 943 ++++++++++++++++++ opt/ktouch/docs/check_save.md | 53 + opt/ktouch/docs/demo.md | 53 + opt/ktouch/docs/gdk_test.md | 26 + opt/ktouch/docs/screen_binder.md | 89 ++ opt/ktouch/docs/screen_ds.md | 70 ++ opt/ktouch/docs/screen_ds_once.md | 50 + opt/ktouch/docs/touch_ds.md | 73 ++ opt/ktouch/docs/touch_listen.md | 99 ++ opt/ktouch/docs/touch_set.md | 152 +++ opt/ktouch/docs/usb_ds.md | 77 ++ opt/ktouch/forest-light.tcl | 541 ++++++++++ .../forest-light/border-accent-hover.png | Bin 0 -> 445 bytes opt/ktouch/forest-light/border-accent.png | Bin 0 -> 463 bytes opt/ktouch/forest-light/border-basic.png | Bin 0 -> 311 bytes opt/ktouch/forest-light/border-hover.png | Bin 0 -> 324 bytes opt/ktouch/forest-light/border-invalid.png | Bin 0 -> 444 bytes opt/ktouch/forest-light/card.png | Bin 0 -> 353 bytes opt/ktouch/forest-light/check-accent.png | Bin 0 -> 526 bytes opt/ktouch/forest-light/check-basic.png | Bin 0 -> 390 bytes opt/ktouch/forest-light/check-hover.png | Bin 0 -> 531 bytes opt/ktouch/forest-light/check-tri-accent.png | Bin 0 -> 358 bytes opt/ktouch/forest-light/check-tri-basic.png | Bin 0 -> 281 bytes opt/ktouch/forest-light/check-tri-hover.png | Bin 0 -> 358 bytes .../forest-light/check-unsel-accent.png | Bin 0 -> 403 bytes opt/ktouch/forest-light/check-unsel-basic.png | Bin 0 -> 311 bytes opt/ktouch/forest-light/check-unsel-hover.png | Bin 0 -> 398 bytes .../forest-light/check-unsel-pressed.png | Bin 0 -> 335 bytes .../forest-light/combo-button-basic.png | Bin 0 -> 247 bytes .../forest-light/combo-button-focus.png | Bin 0 -> 260 bytes .../forest-light/combo-button-hover.png | Bin 0 -> 244 bytes opt/ktouch/forest-light/down-focus.png | Bin 0 -> 200 bytes opt/ktouch/forest-light/down.png | Bin 0 -> 266 bytes opt/ktouch/forest-light/empty.png | Bin 0 -> 130 bytes opt/ktouch/forest-light/hor-accent.png | Bin 0 -> 154 bytes opt/ktouch/forest-light/hor-basic.png | Bin 0 -> 157 bytes opt/ktouch/forest-light/hor-hover.png | Bin 0 -> 154 bytes opt/ktouch/forest-light/notebook.png | Bin 0 -> 190 bytes opt/ktouch/forest-light/off-accent.png | Bin 0 -> 765 bytes opt/ktouch/forest-light/off-basic.png | Bin 0 -> 547 bytes opt/ktouch/forest-light/off-hover.png | Bin 0 -> 771 bytes opt/ktouch/forest-light/on-accent.png | Bin 0 -> 754 bytes opt/ktouch/forest-light/on-basic.png | Bin 0 -> 538 bytes opt/ktouch/forest-light/on-hover.png | Bin 0 -> 764 bytes opt/ktouch/forest-light/radio-accent.png | Bin 0 -> 674 bytes opt/ktouch/forest-light/radio-basic.png | Bin 0 -> 486 bytes opt/ktouch/forest-light/radio-hover.png | Bin 0 -> 679 bytes opt/ktouch/forest-light/radio-tri-accent.png | Bin 0 -> 549 bytes opt/ktouch/forest-light/radio-tri-basic.png | Bin 0 -> 390 bytes opt/ktouch/forest-light/radio-tri-hover.png | Bin 0 -> 550 bytes .../forest-light/radio-unsel-accent.png | Bin 0 -> 676 bytes opt/ktouch/forest-light/radio-unsel-basic.png | Bin 0 -> 504 bytes opt/ktouch/forest-light/radio-unsel-hover.png | Bin 0 -> 674 bytes .../forest-light/radio-unsel-pressed.png | Bin 0 -> 512 bytes opt/ktouch/forest-light/rect-accent-hover.png | Bin 0 -> 335 bytes opt/ktouch/forest-light/rect-accent.png | Bin 0 -> 335 bytes opt/ktouch/forest-light/rect-basic.png | Bin 0 -> 254 bytes opt/ktouch/forest-light/rect-hover.png | Bin 0 -> 272 bytes opt/ktouch/forest-light/right-focus.png | Bin 0 -> 190 bytes opt/ktouch/forest-light/right.png | Bin 0 -> 284 bytes opt/ktouch/forest-light/scale-hor.png | Bin 0 -> 161 bytes opt/ktouch/forest-light/scale-vert.png | Bin 0 -> 162 bytes opt/ktouch/forest-light/separator.png | Bin 0 -> 128 bytes opt/ktouch/forest-light/sizegrip.png | Bin 0 -> 471 bytes .../forest-light/spin-button-down-basic.png | Bin 0 -> 156 bytes .../forest-light/spin-button-down-focus.png | Bin 0 -> 163 bytes opt/ktouch/forest-light/spin-button-up.png | Bin 0 -> 223 bytes opt/ktouch/forest-light/tab-accent.png | Bin 0 -> 184 bytes opt/ktouch/forest-light/tab-basic.png | Bin 0 -> 183 bytes opt/ktouch/forest-light/tab-hover.png | Bin 0 -> 184 bytes opt/ktouch/forest-light/thumb-hor-accent.png | Bin 0 -> 316 bytes opt/ktouch/forest-light/thumb-hor-basic.png | Bin 0 -> 242 bytes opt/ktouch/forest-light/thumb-hor-hover.png | Bin 0 -> 316 bytes opt/ktouch/forest-light/thumb-vert-accent.png | Bin 0 -> 311 bytes opt/ktouch/forest-light/thumb-vert-basic.png | Bin 0 -> 234 bytes opt/ktouch/forest-light/thumb-vert-hover.png | Bin 0 -> 310 bytes opt/ktouch/forest-light/tree-basic.png | Bin 0 -> 149 bytes opt/ktouch/forest-light/tree-pressed.png | Bin 0 -> 168 bytes opt/ktouch/forest-light/up.png | Bin 0 -> 278 bytes opt/ktouch/forest-light/vert-accent.png | Bin 0 -> 158 bytes opt/ktouch/forest-light/vert-basic.png | Bin 0 -> 158 bytes opt/ktouch/forest-light/vert-hover.png | Bin 0 -> 158 bytes opt/ktouch/kscreen-debug | 170 ++++ opt/ktouch/kscreen-fix-daemon | 308 ++++++ opt/ktouch/kscreen-log | 64 ++ opt/ktouch/kscreen-remap | 52 + opt/ktouch/kscreen-remap-daemon | 539 ++++++++++ opt/ktouch/kscreen-setup | 141 +++ opt/ktouch/src/Makefile | 53 + opt/ktouch/src/check_save.c | 217 ++++ opt/ktouch/src/demo.c | 395 ++++++++ opt/ktouch/src/display_control.py | 677 +++++++++++++ opt/ktouch/src/display_set | 535 ++++++++++ opt/ktouch/src/gdk_test.c | 8 + opt/ktouch/src/kdisplay.py | 818 +++++++++++++++ opt/ktouch/src/kscreen_remap_daemon.py37 | 350 +++++++ opt/ktouch/src/readme.txt | 99 ++ opt/ktouch/src/screen_binder.c | 618 ++++++++++++ opt/ktouch/src/screen_ds.c | 247 +++++ opt/ktouch/src/screen_ds_once.c | 223 +++++ opt/ktouch/src/touch_dis_table.txt | 2 + opt/ktouch/src/touch_ds.c | 604 +++++++++++ opt/ktouch/src/touch_listen.c | 424 ++++++++ opt/ktouch/src/touch_listen.h | 39 + opt/ktouch/src/touch_set.c | 849 ++++++++++++++++ opt/ktouch/src/touch_set.workfine.c | 754 ++++++++++++++ opt/ktouch/src/usb_ds.c | 377 +++++++ 121 files changed, 11161 insertions(+) create mode 100644 .gitignore create mode 100644 DEBIAN/control create mode 100644 DEBIAN/postinst create mode 100644 DEBIAN/postrm create mode 100644 DEBIAN/preinst create mode 100644 DEBIAN/prerm create mode 100644 LICENSE create mode 100644 README.md create mode 100644 opt/ktouch/check_save create mode 100644 opt/ktouch/desktops/screen-config.png create mode 100644 opt/ktouch/desktops/screen-remap.png create mode 100644 opt/ktouch/desktops/touchscreen-auto.desktop create mode 100644 opt/ktouch/desktops/touchscreen-config.desktop create mode 100644 opt/ktouch/desktops/touchscreen-now.desktop create mode 100644 opt/ktouch/display_monitor.py create mode 100644 opt/ktouch/docs/check_save.md create mode 100644 opt/ktouch/docs/demo.md create mode 100644 opt/ktouch/docs/gdk_test.md create mode 100644 opt/ktouch/docs/screen_binder.md create mode 100644 opt/ktouch/docs/screen_ds.md create mode 100644 opt/ktouch/docs/screen_ds_once.md create mode 100644 opt/ktouch/docs/touch_ds.md create mode 100644 opt/ktouch/docs/touch_listen.md create mode 100644 opt/ktouch/docs/touch_set.md create mode 100644 opt/ktouch/docs/usb_ds.md create mode 100644 opt/ktouch/forest-light.tcl create mode 100644 opt/ktouch/forest-light/border-accent-hover.png create mode 100644 opt/ktouch/forest-light/border-accent.png create mode 100644 opt/ktouch/forest-light/border-basic.png create mode 100644 opt/ktouch/forest-light/border-hover.png create mode 100644 opt/ktouch/forest-light/border-invalid.png create mode 100644 opt/ktouch/forest-light/card.png create mode 100644 opt/ktouch/forest-light/check-accent.png create mode 100644 opt/ktouch/forest-light/check-basic.png create mode 100644 opt/ktouch/forest-light/check-hover.png create mode 100644 opt/ktouch/forest-light/check-tri-accent.png create mode 100644 opt/ktouch/forest-light/check-tri-basic.png create mode 100644 opt/ktouch/forest-light/check-tri-hover.png create mode 100644 opt/ktouch/forest-light/check-unsel-accent.png create mode 100644 opt/ktouch/forest-light/check-unsel-basic.png create mode 100644 opt/ktouch/forest-light/check-unsel-hover.png create mode 100644 opt/ktouch/forest-light/check-unsel-pressed.png create mode 100644 opt/ktouch/forest-light/combo-button-basic.png create mode 100644 opt/ktouch/forest-light/combo-button-focus.png create mode 100644 opt/ktouch/forest-light/combo-button-hover.png create mode 100644 opt/ktouch/forest-light/down-focus.png create mode 100644 opt/ktouch/forest-light/down.png create mode 100644 opt/ktouch/forest-light/empty.png create mode 100644 opt/ktouch/forest-light/hor-accent.png create mode 100644 opt/ktouch/forest-light/hor-basic.png create mode 100644 opt/ktouch/forest-light/hor-hover.png create mode 100644 opt/ktouch/forest-light/notebook.png create mode 100644 opt/ktouch/forest-light/off-accent.png create mode 100644 opt/ktouch/forest-light/off-basic.png create mode 100644 opt/ktouch/forest-light/off-hover.png create mode 100644 opt/ktouch/forest-light/on-accent.png create mode 100644 opt/ktouch/forest-light/on-basic.png create mode 100644 opt/ktouch/forest-light/on-hover.png create mode 100644 opt/ktouch/forest-light/radio-accent.png create mode 100644 opt/ktouch/forest-light/radio-basic.png create mode 100644 opt/ktouch/forest-light/radio-hover.png create mode 100644 opt/ktouch/forest-light/radio-tri-accent.png create mode 100644 opt/ktouch/forest-light/radio-tri-basic.png create mode 100644 opt/ktouch/forest-light/radio-tri-hover.png create mode 100644 opt/ktouch/forest-light/radio-unsel-accent.png create mode 100644 opt/ktouch/forest-light/radio-unsel-basic.png create mode 100644 opt/ktouch/forest-light/radio-unsel-hover.png create mode 100644 opt/ktouch/forest-light/radio-unsel-pressed.png create mode 100644 opt/ktouch/forest-light/rect-accent-hover.png create mode 100644 opt/ktouch/forest-light/rect-accent.png create mode 100644 opt/ktouch/forest-light/rect-basic.png create mode 100644 opt/ktouch/forest-light/rect-hover.png create mode 100644 opt/ktouch/forest-light/right-focus.png create mode 100644 opt/ktouch/forest-light/right.png create mode 100644 opt/ktouch/forest-light/scale-hor.png create mode 100644 opt/ktouch/forest-light/scale-vert.png create mode 100644 opt/ktouch/forest-light/separator.png create mode 100644 opt/ktouch/forest-light/sizegrip.png create mode 100644 opt/ktouch/forest-light/spin-button-down-basic.png create mode 100644 opt/ktouch/forest-light/spin-button-down-focus.png create mode 100644 opt/ktouch/forest-light/spin-button-up.png create mode 100644 opt/ktouch/forest-light/tab-accent.png create mode 100644 opt/ktouch/forest-light/tab-basic.png create mode 100644 opt/ktouch/forest-light/tab-hover.png create mode 100644 opt/ktouch/forest-light/thumb-hor-accent.png create mode 100644 opt/ktouch/forest-light/thumb-hor-basic.png create mode 100644 opt/ktouch/forest-light/thumb-hor-hover.png create mode 100644 opt/ktouch/forest-light/thumb-vert-accent.png create mode 100644 opt/ktouch/forest-light/thumb-vert-basic.png create mode 100644 opt/ktouch/forest-light/thumb-vert-hover.png create mode 100644 opt/ktouch/forest-light/tree-basic.png create mode 100644 opt/ktouch/forest-light/tree-pressed.png create mode 100644 opt/ktouch/forest-light/up.png create mode 100644 opt/ktouch/forest-light/vert-accent.png create mode 100644 opt/ktouch/forest-light/vert-basic.png create mode 100644 opt/ktouch/forest-light/vert-hover.png create mode 100644 opt/ktouch/kscreen-debug create mode 100644 opt/ktouch/kscreen-fix-daemon create mode 100644 opt/ktouch/kscreen-log create mode 100644 opt/ktouch/kscreen-remap create mode 100644 opt/ktouch/kscreen-remap-daemon create mode 100644 opt/ktouch/kscreen-setup create mode 100644 opt/ktouch/src/Makefile create mode 100644 opt/ktouch/src/check_save.c create mode 100644 opt/ktouch/src/demo.c create mode 100644 opt/ktouch/src/display_control.py create mode 100644 opt/ktouch/src/display_set create mode 100644 opt/ktouch/src/gdk_test.c create mode 100644 opt/ktouch/src/kdisplay.py create mode 100644 opt/ktouch/src/kscreen_remap_daemon.py37 create mode 100644 opt/ktouch/src/readme.txt create mode 100644 opt/ktouch/src/screen_binder.c create mode 100644 opt/ktouch/src/screen_ds.c create mode 100644 opt/ktouch/src/screen_ds_once.c create mode 100644 opt/ktouch/src/touch_dis_table.txt create mode 100644 opt/ktouch/src/touch_ds.c create mode 100644 opt/ktouch/src/touch_listen.c create mode 100644 opt/ktouch/src/touch_listen.h create mode 100644 opt/ktouch/src/touch_set.c create mode 100644 opt/ktouch/src/touch_set.workfine.c create mode 100644 opt/ktouch/src/usb_ds.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3f5d9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# ---> C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +.vscode + +# 编译好的二进制文件 +kdisplay +screen_binder +screen_ds +screen_ds_once +touch_ds +touch_set +usb_ds diff --git a/DEBIAN/control b/DEBIAN/control new file mode 100644 index 0000000..ef8e44e --- /dev/null +++ b/DEBIAN/control @@ -0,0 +1,8 @@ +Package: ktouch +Version: 2.0 +Architecture: arm64 +Maintainer: Kushidou +Depends: xinput, python3 +Section: x11 +Priority: optional +Description: 设置触摸屏仅需简单的点击屏幕. diff --git a/DEBIAN/postinst b/DEBIAN/postinst new file mode 100644 index 0000000..6578a1d --- /dev/null +++ b/DEBIAN/postinst @@ -0,0 +1,136 @@ +#!/bin/sh + +BUILDDATE="2025年 09月 25日" +KTVERSION="2.0" +MYDIR=/opt/ktouch +APPDIR=$MYDIR/desktops +# AUTODIR=/etc/xdg/autostart +MENUDIR=/usr/share/applications +BINDIR=/usr/bin +log_path=${MYDIR} +inslog_path=${MYDIR}/install.log + +LOGINUSER=$(whoami) +NUSER=${SUDO_USER} + + + +set_autostart() +{ + SUSER=$NUSER + if [ "$NUSER" = "" ];then + echo "注意!用户是空,应该使用sudo dpkg 来安装。" + echo "用户名为[$SUSER], 免root操作触摸屏和开机自启动都不能正常工作。" >> $log_path + SUSER="1000" + fi + + + # 生成服务文件内容 + SERVICE_CONTENT="[Unit] + Description=Kushidou Touchscreen Remap Daemon + After=display-manager.service + Requires=display-manager.service + + [Service] + Type=simple + User=$SUSER + Group=$SUSER + Environment=DISPLAY=:0 + #Environment=XAUTHORITY=/home/$NUSER/.Xauthority + ExecStart=/usr/bin/python3 /opt/ktouch/kscreen-remap-daemon + Restart=always + RestartSec=5 + StartLimitInterval=0 + + [Install] + WantedBy=graphical.target" + + # 写入服务文件 + SERVICE_FILE="/etc/systemd/system/ktouch_daemon.service" + echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { + echo "写入服务文件失败: $SERVICE_FILE" + exit 1 + } + + # 重新加载systemd配置 + systemctl daemon-reload || { + echo "重新加载systemd配置失败。" + exit 1 + } + + # 启用服务自启动 + systemctl enable ktouch_daemon.service || { + echo "启用服务自启动失败。" + exit 1 + } + + # 开启服务 + systemctl start ktouch_daemon.service || { + echo "启用服务失败。" + exit 1 + } + + echo "服务已成功配置并启动。" +} + + + + +echo "==========INSTALL===========" >> ${APPDIR}/install.log + +echo "=>即将安装触摸屏设置软件ktouch v2">> ${inslog_path} +echo "build date: $BUILDDATE" >> ${inslog_path} +echo "ver: $KTVERSION" >> ${inslog_path} +echo "install: $(date)" >> ${inslog_path} +# start menu +install -p -m 0755 -o root -g root ${APPDIR}/touchscreen-config.desktop ${MENUDIR} +install -p -m 0755 -o root -g root ${APPDIR}/touchscreen-now.desktop ${MENUDIR} + +install -p -m 0755 -o root -g root ${APPDIR}/touchscreen-auto.desktop /etc/xdg/autostart + + +#ICOS +cp ${APPDIR}/*.png /usr/share/pixmaps/ + +#USR/BIN +if [ -f ${BINDIR}/kscreen-remap ];then + rm -f ${BINDIR}/kscreen-remap +fi +if [ -f ${BINDIR}/kscreen-remap ];then + rm -f ${BINDIR}/kscreen-kscreen-remap-daemon +fi +if [ -f ${BINDIR}/kscreen-remap ];then + rm -f ${BINDIR}/kscreen-setup +fi +if [ -f ${BINDIR}/kscreen-remap ];then + rm -f ${BINDIR}/kscreen-log +fi + +ln -s /opt/ktouch/kscreen-remap /usr/bin/kscreen-remap +ln -s /opt/ktouch/kscreen-remap-daemon /usr/bin/kscreen-remap-daemon +ln -s /opt/ktouch/kscreen-setup /usr/bin/kscreen-setup +ln -s /opt/ktouch/kscreen-log /usr/bin/kscreen-log +ln -s /opt/ktouch/kscreen-debug /usr/bin/kscreen-debug +ln -s /opt/ktouch/kdisplay /usr/bin/kdisplay + +chmod 0777 /opt/ktouch/* -R + +# 将用户加入input组 +usermod -a -G input $NUSER +usermod -a -G dialout $NUSER + +# 设置自启动 +set_autostart + +# 禁用UOS自动触摸屏弹窗 +if [ -f /usr/lib/deepin-daemon/dde-touchscreen-dialog ];then + mv /usr/lib/deepin-daemon/dde-touchscreen-dialog /usr/lib/deepin-daemon/dde-touchscreen-dialog.disable +fi + +# 禁用麒麟V10触摸设置工具 +if [ -d /var/screen ];then + mv /var/screen /var/screen.disable +fi + +echo "安装已经完成。如果是首次安装,请重启电脑,否则无法打开触摸屏设备" + diff --git a/DEBIAN/postrm b/DEBIAN/postrm new file mode 100644 index 0000000..82eef6b --- /dev/null +++ b/DEBIAN/postrm @@ -0,0 +1,70 @@ +#!/bin/sh + +APPDIR=/opt/ktouch/desktops +AUTODIR=/etc/xdg/autostart +MENUDIR=/usr/share/applications + +inslog_path="/opt/ktouch/install.log" + +BUILDDATE="2025年 09月 25日" +KTVERSION="2.0" + + +echo "==========REMOVE===========" >> ${APPDIR}/install.log + +echo "=>即将移除ktouch v2">> ${inslog_path} +echo "build date: $BUILDDATE" >> ${inslog_path} +echo "ver: $KTVERSION" >> ${inslog_path} +echo "remove_date: $(date)" >> ${inslog_path} + + + +# desktop +for USERNAME in $(cat /etc/passwd | awk -F: '$3>=500' | cut -d : -f 1) +do + [ -f /home/$USERNAME/Desktop/touchscreen-config.desktop ] && rm -f /home/$USERNAME/Desktop/touchscreen-config.desktop + [ -f /home/$USERNAME/Desktop/touchscreen-now.desktop ] && rm -f /home/$USERNAME/Desktop/touchscreen-now.desktop + [ -f /home/$USERNAME/桌面/touchscreen-config.desktop ] && rm -f /home/$USERNAME/桌面/touchscreen-config.desktop + [ -f /home/$USERNAME/桌面/touchscreen-now.desktop ] && rm -f /home/$USERNAME/桌面/touchscreen-now.desktop + +done + +# start menu +rm $MENUDIR/touchscreen-config.desktop >/dev/null 2>&1 +rm $MENUDIR/touchscreen-now.desktop >/dev/null 2>&1 + + +# autostart +rm /etc/xdg/autostart/touchscreen-auto.desktop >/dev/null 2>&1 +systemctl stop ktouch_daemon +systemctl disable ktouch_daemon +rm /etc/systemd/system/ktouch_daemon.service -f >/dev/null 2>&1 +systemctl daemon-reload + +#ICOS +rm /usr/share/pixmaps/screen-config.png >/dev/null 2>&1 +rm /usr/share/pixmaps/screen-remap.png >/dev/null 2>&1 + +#USR/BIN +rm -f /usr/bin/kscreen-remap >/dev/null 2>&1 +rm -f /usr/bin/kscreen-setup >/dev/null 2>&1 +rm -f /usr/bin/kscreen-log > dev/null 2>&1 +rm -f /usr/bin/kscreen-debug > dev/null 2>&1 +rm -f /usr/bin/kscreen-remap-daemon > dev/null 2>&1 +rm -f /usr/bin/kdisplay > dev/null 2>&1 + +# 解除禁用UOS自动触摸屏弹窗 +if [ -f /usr/lib/deepin-daemon/dde-touchscreen-dialog.disable ];then + mv /usr/lib/deepin-daemon/dde-touchscreen-dialog.disable /usr/lib/deepin-daemon/dde-touchscreen-dialog +fi + +# 清空UOS的触摸屏设置信息 +if [ -f /usr/bin/gsettings ];then + gsettings set com.deepin.dde.display map-output '' > /dev/null 2>&1 +fi + +# 解除禁用麒麟V10触摸设置工具 +if [ -d /var/screen.disable ];then + mv /var/screen.disable /var/screen +fi + diff --git a/DEBIAN/preinst b/DEBIAN/preinst new file mode 100644 index 0000000..c16ee5b --- /dev/null +++ b/DEBIAN/preinst @@ -0,0 +1,58 @@ +#!/bin/bash + +# 2024.05.28 决定将kushidoutouch全部切换到ktouch,这个脚本用于切换。 +# 2025.09.09 ktouch V2基本开发完成,不再需要原来的目录 + +KTVERSION="2.0" +MYDIR=/opt/ktouch +APPDIR=$MYDIR/desktops +AUTODIR=/etc/xdg/autostart +MENUDIR=/usr/share/applications +BINDIR=/usr/bin +log_path="/var/log/ktouch/touchscreen.log" +inslog_path="/var/log/ktouch/install.log" + + +# start menu +if [ -f ${MENUDIR}/touchscreen-config.desktop ];then + rm -f ${MENUDIR}/touchscreen-config.desktop >/dev/null 2>&1 +fi + +if [ -f ${MENUDIR}/touchscreen-now.desktop ];then + rm -f ${MENUDIR}/touchscreen-now.desktop >/dev/null 2>&1 +fi + + + +# desktop +for USERNAME in $(cat /etc/passwd | awk -F: '$3>=500' | cut -d : -f 1) +do + if [ -d "/home/${USERNAME}/Desktop" ];then + if [ -f /home/${USERNAME}/Desktop/touchscreen-config.desktop ];then + rm -f /home/${USERNAME}/Desktop/touchscreen-config.desktop + fi + if [ -f /home/${USERNAME}/Desktop/touchscreen-now.desktop ];then + rm -f /home/${USERNAME}/Desktop/touchscreen-now.desktop + fi + + fi + if [ -d "/home/${USERNAME}/桌面" ];then + if [ -f /home/${USERNAME}/桌面/touchscreen-config.desktop ];then + rm -f /home/${USERNAME}/桌面/touchscreen-config.desktop + fi + if [ -f /home/${USERNAME}/桌面/touchscreen-now.desktop ];then + rm -f /home/${USERNAME}/桌面/touchscreen-now.desktop + fi + fi +done + +# autostart +rm $AUTODIR/touchscreen-autoremap.desktop >/dev/null 2>&1 +rm $AUTODIR/touchscreen-powerup.desktop >/dev/null 2>&1 + +#USR/BIN +rm -f /usr/bin/kscreen-remap >/dev/null 2>&1 +rm -f /usr/bin/kscreen-setup >/dev/null 2>&1 +rm -f /usr/bin/kscreen-log > dev/null 2>&1 + + diff --git a/DEBIAN/prerm b/DEBIAN/prerm new file mode 100644 index 0000000..3544c03 --- /dev/null +++ b/DEBIAN/prerm @@ -0,0 +1,5 @@ +#!/bin/bash +inslog_path="/var/log/ktouch/install.log" + +systemctl stop ktouch_daemon.service +systemctl disable ktouch_daemon.service \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfff241 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 kushidou + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..50d74e0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ktouchV2 + +适配UOS和银河麒麟的触摸屏设置工具。 \ No newline at end of file diff --git a/opt/ktouch/check_save b/opt/ktouch/check_save new file mode 100644 index 0000000000000000000000000000000000000000..9b39b696b0547f2e98f79fb4d06428c12deb090b GIT binary patch literal 20736 zcmeHP3v^V~)jl)9fM_J)8Qv2RiaaI(kpfb=32zHRLt3G>-b`jD$w)F2XC@E?Z3HT` zDr5u!pB|sv*1y*8 zS=^knzq8NY`|R_$_s+SSAIzFR+v#vHm7MHpMpSve!6EUNvBUE?DY40{8#CBoHh^UV zxd@lRYbX~bl^>R(Ro+eVWg+iEWv2w3hA7BZ_eT6B zTbss-`lfNkyrA+y+!)3d$y~J~^Qhf!*>1ONr}83czsgnrNDN(rWc?tg0g7mYGB%U{ zP3B7Nx7wmU(s!XLS2C8O9m%lkU!Al=<;tIhHGzu48sE5@K(M}HoW~RPR!^Gfj)dIf zh5aO-c;{UAZLW#Ntt&5y`rarLM)hY}ikEp`88l$&-~*1Fcbm7}IpMzS5vZf_BEMXW zi#g}!#*E>2{Bc9KYBqaOQxhgYR(V0J(Kzga_0HmX$64dW%+>A0h8^p=9VswIOkitr zd(OM4g!Q>Bo6RCwq+iMFj`F3rF2mIWS5I7IQ$Jk&aZwtKs{q$zEV=WF88dqi&c1j@ zUjL=AorB)o_|KY~yb~O^cDrHj>c8goC@uUTC%*Gz-+SkN(iZ?_X^%Vw5#9jS04J|_#=+pFE9OY+sc*qKK9A<+qSMAe_&0{jpzRyeR65nnqxOF z8MNTpOBX!z-aV`Lm9Ou7af&hYo7FvUEt&Pk2j74AmW$tca9a7CNn^(Ly5|>*&W>Gj zXmOWg?;Lvj`hlab|3`l7oWYg*Zn^uT%lf{*{QQctX%fqyUo&m^XWI1Z5gYq=Vo_(3|A390VK#ct+Q?CB zRrOQly&;GEv)|&Mkv9GPm5n{Wv+390ZS4804gVn)UJbV4f6E5%w85{ivGcr*oek2SM=kTbn+^XXHumS(`14&GJ0G?g$D6tQ ze#~mm%QpOX+1UB6jUQIq*mJdw{nKsa-(%y4@7mZo#|Ce(k^hd3ol7`=2|L@{fW%}P zXk-66X{Xhn49^)=(PiVVq-TpFR%LqX>S_XBPc#q;npOU&8C_ZDXH~vsX2cuz`vq1T z3I?K~uvu4AUu6b8wO~kTcXNFp3Mdk&3VLcxZzve_d!we$6ZJ6j+T~`czbX)k`olA7 zJdub$VwD~=rTnxSrMJo-s`W?1E1@G)aTCd|4~G4fRzW_0rKi3o%FKCX3rwFsEY+4R zm{Ai7`pY~OHGXEARgh{%qMmTnGzDu$MmQ;@k1GN}UucCH^siu4CCLap=JR_)VNcZW z;}tcY3V)5}N$}SCBN0!P-}D7MH6b`KRYcP1V66x>m?X10R2!=D2mPUXIK`@t_Y&T| z1>=A^_&e$e1pQ&t;}bTl2>3{9q#6c$YHBzaNv`vFF|O2gOT&Z>W@R845d8JgC>#Z2 z(bs6T$@|4?L=Tg_;JSSNNHiS62&EcZ7m5UEYH?ilKcURRyUg#eGd&fd<)T~q#8k?p zgmAqONPSXO!Yc9|9-;l_im<0n^aDI88k6oK=88}lGX-7rMXPn=Yn2(=Kt}^n%pkHl zsqCiuNHkEnl9}*ZlrJt{VsX(@3xvi^a2I7-Y}5(j5Hnimq9tg0tNj?PKoG+f^n&z6 zxcItgb=dFm34v(trP3tkZ-Dl?`e+0b94*k&TfNMz^aN@cJc{wg==kdESgpSny=JwT z6(KK!U;M%4sDlf_ta3#-5TzADV&w{{#l&K0!P$a!;Xp82$ttliu*~l&y7sDiPuRyy zzt{)NibzDxJu_LRCCDj%(8tR%N5r9v>YBuzDYTBO zhPXw5N7PW6BoIYqk#x$*>s6ft)I7eoGUfvFw%As@CT6i;0HZ|pFJrG(?}MV=nW%3q zEEas=Puj*3&sp@cs@|7~Q9Yj$p2J(pKSe^06S(f5ZW^xM52;12hSR%~O8FX2?`tZ# zG+aF!Q{5;Hzg#gRFVgUDgMPCJm=|XO*^Vcpn8Kk83!+TdTBN z!!J=F^8FfaX!rpQr}uA_4r}-{1tNb@!>4HYYa0Fy4L_#g`a689hWFL@+ckWMhM&=J z`dvV!vl{MJAm#G8mFE978s1IA_4lS+4Ii%Y=WBQ`4R>jHk%o`b@JSk8q~Sv~e6ofQ z({Mw>)o*K5Jy*l|?=-T_A`Mr23so)A@QIR_vT_Yqznu_Xt>JkD$W*7{`tM#18tzie z$Qw0$kcMy2@Ch2;q~T*V{M@SN_ngaOvG*C-40MG?_XNF0qx*v1snG*KAJOO` zppR?xFwjm%QvXQM!!&v{=n{=~gRao%382?#^d!(bHF_%OBN}}T=;In)0@~?J>YojI zm_}aP62(8o1;4QOYVr2acV57X!$gD%nNji4(u`X110G^V z=p!2aFzDkNy%V(aqNM&uKo8UCX3!-Xy%%(aM*kA@8jXGm^iGX_2J{h)J_P!>Mjrv~ z%uee6E$Cqy{SxRBK@Vn)M~C2Q%VN#%W*HsFhqKs8th>Y5Lf*sG#le<5(Al}|4qUYU zT$dY^vDOhHUoukUjaZZH%$yETPu$Si*-x$4+p!)|_gcZ>>=?sUFo?x)zyJO8$?T&& zjFr6r9UioCF|m%R9j#?1Q(AkpE%6-o$~P|M#s5TNBACty8WziQp~}O4%qc%EBZqA;0R{P zj?T^})V}0m)8C|h>6oln_MTgHkn4;0U>(5rv)2BxpYOR=eFZ0^R{e2jPTYhXY)zm<2AmcpikqXHm@Tm8_%u!HSdG! z^YcSl3+^Gk(Wlp#t82`OKJ6IV*wUKiXlDPo?R7kr-pS6~`nti`oeXujKYi5bU}YD| z|0L-jYP8@PG%pTayq(cC23ajTfKO*^*_h8e%Y<#hzsf%}9@O_8y`-O917dmTAD0o& zj=`3eR}@{|HP#FE{SNwsKYBIagL0efw-`Uwcj5mu`Nn`)2l$nI8UqJ(JZaIfU(>M? z<^Rg4uOlybqs&$hOO!3luRrAM*@=o-32mG^H<9Sr$={N_T>EOBAXsOV6c4<6k zz(aHCedJ2d16qB%tiMrfPx_P{t+IZktj|WD;FBiDA~*U?^J!r0BFOK8vChIp{t<1n zoBxEi#g4{Onm22x{czSXP0EfB$9h5i?as#1wNf_uYoe?tf4zYE9pGCAjO%I~80$^E zUt=Amk}v)>qvL65Bl%;#hCdFv7`op=-dEv}_Y2Ulx0Lp~VYqfc#$Muutqk<_q+<;0 zxLCCrV{|->dh+8t$g>m*K<+l!s)uD{_CC23N*e{lUrP0v<{?~CS`PC?=$Oq3d zl1b|a<3aO-_Qyt=>sZ@6o))-^v6pB+HgJ#p7kI?J@Rr;co=VvlXb(ku+-v0Cuu$v` zr%4ay1kDAK^{&|4_tSlt?r#k9jJXD5jXIi#Wi=j!|BsSCIUXMzTYzz(xph64V|4rh zIr=r4>oU4R2H*2BN5!5``@jbobqwJ;Dx5+`Na<)C5~KM@I<5hhl#6=`+15HFwn)R_ zuNKE@W3%=C^-W<{GjH$Wb_su=oP~ZWxpwVo&v3wo)~u{%7i2w;y>Tt=Q!^Yfs-yK! zK735s0=fKNdUfV|De*hHN#A@K?_%&?dV##xgE#jAd2ax3?+fHL!JE(cK1cn0>vPN- z>;bg)`5dJE9%cBC@AZe(7=JM zGJ%uK-=7oDO1zHtf`#dO!CBZQ_JWt>UZ6dT6k-g;^A!5zioFb88b1fdkIU58ecZ*@ z}EIQC>eBLDwT5SzBCYJ?Da=U-=!$WmA{~a_ROECwwQlJQy*KqurgY=@_9Eo9yV)K zJh-~??d4cC$-Y;oeld?6QO}4h^&v)=MJGV-V-zPUX_uLD=f4tbS2On=V zJy|j-wxt4>v(N88^JeY&6wgsOs~`Ot=4s2nI=5{(KVx&5gFVp;e%f_Qg4u)ZB35|Ic*#6$G=neuRHz2yH6e5i^7TLcbvR^--plr zr2C0ypFO#5)2ZfdZJTzTUVE@@)4mhW{_N!DyH6Z^@V&-0j0p*hd2wjD3`cHlo3@^O zYC~JgQ>Pmn+txqBPHu~z{MEhf8=po++vA(sTJ9CH)o-YF^|HI{4H{I3U1+C74CU+6E zx{p33?Neuv>I_sr!z6jKTdZB4dpik4dQrCTEy%>T%>5)?lxy&kI>T=7A!udyT*;@- z$dvx`ouWUxC4Z-+mEJRw9wO_dnE$m*eV_hi3Ud1xoaL+h|CS%D<3{Uy&WssTU8BBT zQ6G%f7tRj^J>ih+D)&Tp(YQ&=@Hx@=+s03F7fp1JpKyhMT@#ANPb!*JG{q{+LesY_ z%xPFqT0_o6yny@`MV_6b1MHuOs(ToJe#TYo{D#6nbt`bzWP_|E0zCZrq*dH z-kquSoQkJ^&q(!I*!9#pNafE-#KWZG>ECw?IXzh2NJD4k~-HdG7d3%CX`~&SVR}!27YdW&SAn*5{h;wDrJFEA9gBKpenfDTn40B_Th3T@-Tu zbo@I~zIv__Qj{Ewdq(@y*JA~jpN`dRsc5(ltXEIRP7d9q>G7zvTCvQhpE*!eqa7Uu^=vOE+AWALa6MSkXX( zBR+{AveEko(wiY(=Wm=peV;rb<*V;Hx=8u|kod#_LhpkT&*t_^Ld*Kw;Dt8$OdEWu z4PFgA7g2=P`(Qoi&tY4nf2yTTciG5!%mzQi<>#lxoxEhj|0f&#cbJcv=Fi7A{Mm50 zOY@^D_5t2g5n4VLcpm(q_9Zo5vu)&0;{5sSnB2$Id(%`K`3r1tzYTsXa2gkDe91Z+ z{s(RFeKzs{Ty)c7{r@YirW{WYp;;wBK5vb38V3qQnos9k8s6du;gk+TbtP;QuY|Cch2lcn*6_?!)D>+v9BbXJX&ZwEtXV!@mf49wN2X`;oHKwBe7~;6DJK zN$+|a{-4|6tgt>3=0WgPUT>j{f0xnlRl)kgs|ww2`7gcF8;*{5huGylc6q~pDkuO! zUWCV(EBqcr&;AaF$BymEnYCooORty9;!nfQDx#uh(pki zE?)$knRDhZEGeFEE}T7k@vJhlthi+UECm0|TzXycf_XC#k%T~_s+5SkM3h&?NHiYY zhOi}sC5ospD5Vf15kT|>p=CV6F=Kd@X(Cimb?FOY2oVp)Ms5P6SmM zh8D#nBdffTdJ$%a0gw18Ku<;!icmjmpkBtXN=yGlWLiW9Xr(A6PAzN>vkPIFG7t+L zk>PFPxYiWMwO<9dQZOxlr*6O=J(7N;g+7e!!PiUuB3mx5#rbWNGhB1$&l3Kh7RDJoKrebfR^ zH8=AWXbIFyu}AqJ;Wcgc@mNKzw~1iKWNc)j83h%l3D$xlQ=?yPLS6O#@P6}%!VF?l z<5m%YjH7539xUgoH3*nT`(_N`#axes%xX{2hpPWZ12k zNC`h#7L69vtfH8`A^c!$8?$T)+e|Tq0+z8cC$<|3z)WEvTa#l)VZi3Rg^=hA1kL&g zK9kToleAc_5*iRw?VFhczmoyjDT>H1DFL+19a&i$^;95_hDBbjN(deGhwGR-7>fGc z#U=B`MLkuL!VTVCQ6E4^X~4(aL{)nt)y(Z%8H7BMN5i6Kxj!7CfMU)j2@`c;e~pJ4 z$Wk5Rz}S;I#JX$5%4{{oylK=0Ki&M!z z>UmEg^u3SXUY#pUlLlR`7-ep4KNI+;I9pTpmpct04CfF$W5=YyczxhWy{|};U55gl zC93x7oMN(6RD|beN|cc()n1+ZeFqp`DH83~c}BH7uvF)jVByK?zZ-$lS)tOe&OL_6 zAkKBNow8r?seBpg>FiO_>U?CkY;VZ+WIypJ`z6e7L4mre+N*PvcG-TeWK{Z9d)0rk zkJ^_=K6Tzwlq&#r-b(H9jb=i>YQGT}`9!)oao&7Z{t&Nzj|U4+q)Sgf0-?88=Qp|X zKzI=ij+UfZcy|zK3o^aE`kky?8u)pR))+0g2k-&=|45UlzufX$1M8QIoYv7 zR3IrnK_&a6?)amWj?~U8#hmh9!ui7b*AD}>(>i>TE z{{N_KU!)jiZf*Y&aMEw(Q|+f(+N6z5g#(*As! gTu6g(Ex@JhSNjKjhmlD?D@Fy4e?^GY@$OWfyf1 z^%nvQM3;%`DIX}D@tZ-<#e&b=l4seM8E=pM_;oA3-}nF9oqxAke!KMLPXEuJ*}vzP zTr+p7=Dr=hM)y+v`pAavKE@B1&S@?%c_w); z_<+zGKL&$i=dLltoA1%h*AGwLwUW_y)hortvYHP9_DuI@VtB{me}^+}o%7a%`=u01 zH=O-@UC(6}`+_nChAg%#{{kCkF>b9FS&-)NR-Sc*WWaBCMl|97yeUOH93vD9b^}9{ N!PC{xWt~$(6954yoOl2L literal 0 HcmV?d00001 diff --git a/opt/ktouch/desktops/screen-remap.png b/opt/ktouch/desktops/screen-remap.png new file mode 100644 index 0000000000000000000000000000000000000000..7bd197523392e4e26a1f65bd87a60729f45b76b5 GIT binary patch literal 345 zcmV-f0jB}SN|b^{b&4H7zS>fm%lR&`&AKH2tjX8EKmZCz&Hxrj zE&w(`EPw@)Gk^t>3xHDq3t)ld3|K6%O~O%kttJAlw`XV$9`{SZ4*|U_q5y-oH!Q#& zz$R#Lir-f_1vmz&11N;31Sy5-1!#tbHbd`XJOMw*yC|HdYGra;{nlt1 10 or height_diff > 10: + return False, f"分辨率不匹配: 当前 {current_res}, 配置 {target_width}x{target_height}" + + # 检查刷新率(允许±2fps误差) + current_refresh = self.current_settings.get('refresh_rate', 0) + target_refresh = self.config_data['refresh_rate'] + refresh_diff = abs(current_refresh - target_refresh) + + if refresh_diff > 2.0: + return False, f"刷新率不匹配: 当前 {current_refresh:.1f}Hz, 配置 {target_refresh:.1f}Hz" + + # 检查旋转 + current_rotation = self.current_settings.get('rotation', 'normal') + if current_rotation != self.config_data['rotation']: + return False, f"旋转不匹配: 当前 {current_rotation}, 配置 {self.config_data['rotation']}" + + # 检查主屏设置 + current_primary = self.current_settings.get('primary', False) + if current_primary != self.config_data['primary']: + primary_status_current = "是" if current_primary else "否" + primary_status_target = "是" if self.config_data['primary'] else "否" + return False, f"主屏设置不匹配: 当前 {primary_status_current}, 配置 {primary_status_target}" + + # 位置不进行校验 + match_details = [ + f"分辨率: {current_res}", + f"刷新率: {current_refresh:.1f}Hz", + f"旋转: {current_rotation}", + f"主屏: {'是' if current_primary else '否'}", + "位置: 跳过校验" + ] + + return True, " | ".join(match_details) + + def configure_display(self): + """配置显示器""" + if not self.config_data: + logger.error("没有可用的配置数据") + return False + + # 检查显示器是否连接 + if not self.is_display_connected(): + logger.warning(f"显示器 {self.display_name} 未连接,跳过配置") + return True # 返回True表示跳过而不是失败 + + # 获取当前支持的模式 + modes = self.get_display_modes() + if not modes: + logger.error(f"无法获取显示器 {self.display_name} 的模式信息") + return False + + logger.info(f"显示器 {self.display_name} 支持的模式:") + for res, rates in modes.items(): + logger.info(f" {res}: {rates}") + + # 根据模式标志决定行为 + mode_flag = self.config_data.get('mode_flag', 'A') # 默认为A + + if mode_flag == 'F': + # F模式:必须使用cvt生成分辨率 + logger.info("模式标志为F,强制使用cvt生成分辨率") + mode_name = self.create_and_add_mode() + if not mode_name: + logger.error("创建新模式失败") + return False + else: + # A模式:沿用原来的方法 + logger.info("模式标志为A,使用自动模式选择") + mode_name = self.find_matching_mode(modes) + + # 如果不支持,创建新模式 + if not mode_name: + logger.info("显示器不支持该模式,尝试创建新模式...") + mode_name = self.create_and_add_mode() + if not mode_name: + logger.error("创建新模式失败") + return False + else: + logger.info(f"找到匹配模式: {mode_name}") + + # 构建xrandr命令 + cmd_parts = ["xrandr", f"--output {self.display_name}"] + + # 添加模式 + if ' ' in mode_name: + resolution, rate = mode_name.split(' ', 1) + cmd_parts.append(f"--mode {resolution}") + cmd_parts.append(f"--rate {rate}") + else: + cmd_parts.append(f"--mode {mode_name}") + + # 添加位置 + cmd_parts.append(f"--pos {self.config_data['pos_x']}x{self.config_data['pos_y']}") + + # 添加旋转 + cmd_parts.append(f"--rotate {self.config_data['rotation']}") + + # 如果是主屏 + if self.config_data['primary']: + cmd_parts.append("--primary") + + # 启用显示器 + cmd_parts.append("--auto") + + # 执行最终配置命令 + final_cmd = " ".join(cmd_parts) + logger.info(f"执行命令: {final_cmd}") + + if self.run_xrandr_command(final_cmd): + logger.info("显示器配置成功!") + return True + else: + logger.error("显示器配置失败!") + return False + + def check_and_configure(self, config_file_path, is_first=False): + """检查并配置显示器 - 主要入口方法""" + # 解析配置文件 + if not self.parse_config_file(config_file_path): + logger.error(f"解析配置文件失败: {config_file_path}") + return False + + # 检查显示器是否连接 + if not self.is_display_connected(): + logger.warning(f"显示器 {self.display_name} 未连接,跳过配置") + return True # 返回True表示跳过而不是失败 + + # 如果是首次运行,无视显示器配置 + if is_first: + # logger.info("启动程序后首次设置,开始配置显示器") + return self.configure_display() + + # 获取当前设置 + current_settings = self.get_current_display_settings() + if not current_settings: + logger.warning("无法获取当前显示器设置,尝试直接配置...") + return self.configure_display() + + logger.info("当前显示器设置:") + for key, value in current_settings.items(): + logger.info(f" {key}: {value}") + + # 比较设置 + match, message = self.compare_settings() + + if match: + logger.info(f"✓ 设置匹配: {message}") + return True + else: + logger.info(f"✗ 设置不匹配: {message}") + logger.info("开始配置显示器...") + return self.configure_display() + + +class FileMonitor: + def __init__(self): + self.os_version_file = Path("/etc/os-version") + self.screen_file = Path("/tmp/ktouch/screen.txt") + self.settings_dir = Path("/opt/ktouch/display_config") + self.display_update_file = Path("/tmp/ktouch/display_update.txt") + + # 存储文件最后修改时间和内容 + self.last_mod_time = None + self.last_content = None # 新增:存储文件内容 + self.last_display_update_time = 0 + + # 防止重复触发的机制 + self.last_operation_time = None + self.cooldown_period = 15 # 冷却时间15秒 + + # 设置信号处理,用于优雅退出 + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + self.running = True + self.display_checker = DisplayChecker() + + def signal_handler(self, signum, frame): + """处理中断信号""" + logger.info(f"接收到信号 {signum},正在退出...") + self.running = False + + def check_os_version(self): + """检查 /etc/os-version 文件是否存在""" + if not self.os_version_file.exists(): + logger.error(f"文件 {self.os_version_file} 不存在,退出脚本") + sys.exit(1) + logger.info(f"检测到 {self.os_version_file} 文件,继续执行") + + def wait_for_screen_file(self): + """等待 screen.txt 文件生成""" + while not self.screen_file.exists() and self.running: + logger.info(f"等待 {self.screen_file} 文件生成...") + time.sleep(5) + + if not self.running: + return False + + # 获取初始修改时间和内容 + self.last_mod_time = self.screen_file.stat().st_mtime + self.last_content = self.read_file_content(self.screen_file) + logger.info(f"检测到 {self.screen_file} 文件,初始修改时间: {time.ctime(self.last_mod_time)}") + return True + + def read_file_content(self, file_path): + """读取文件内容""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.read().strip() + except Exception as e: + logger.error(f"读取文件内容失败 {file_path}: {e}") + return None + + def is_in_cooldown_period(self): + """检查是否在冷却期内""" + if self.last_operation_time is None: + return False + + elapsed_time = time.time() - self.last_operation_time + return elapsed_time < self.cooldown_period + + def update_file_modification_time(self): + """更新文件修改时间(用于防止重复触发)""" + try: + current_time = time.time() + os.utime(self.screen_file, (current_time, current_time)) + self.last_mod_time = current_time + logger.info(f"已更新文件修改时间以防止重复触发") + return True + except Exception as e: + logger.error(f"更新文件修改时间失败: {e}") + return False + + def check_file_modified(self): + """检查文件是否被修改(同时检查修改时间和内容)""" + if not self.screen_file.exists(): + logger.warning(f"文件 {self.screen_file} 不存在") + return False + + # 检查是否在冷却期内 + if self.is_in_cooldown_period(): + logger.info("在冷却期内,跳过文件修改检查") + return False + + try: + current_mod_time = self.screen_file.stat().st_mtime + current_content = self.read_file_content(self.screen_file) + + # 如果无法读取内容,则跳过 + if current_content is None: + return False + + # 检查修改时间是否变化 + if current_mod_time != self.last_mod_time: + logger.info(f"检测到文件修改时间变化 - 原时间: {time.ctime(self.last_mod_time)}, 新时间: {time.ctime(current_mod_time)}") + + # 检查内容是否变化 + if current_content != self.last_content: + logger.info("检测到文件内容实际发生变化,需要执行分辨率设置") + self.last_mod_time = current_mod_time + self.last_content = current_content + return True + else: + logger.info("文件修改时间变化但内容未变,只更新修改时间") + self.last_mod_time = current_mod_time + # 更新文件修改时间防止重复触发 + self.update_file_modification_time() + return False + + except OSError as e: + logger.error(f"无法获取文件状态: {e}") + + return False + + def check_display_update_file(self): + """检查display_update.txt文件内容是否为1""" + if not self.display_update_file.exists(): + # 文件不存在,忽略检测 + return False + + try: + # 检查文件修改时间,避免重复处理 + current_mtime = self.display_update_file.stat().st_mtime + if current_mtime <= self.last_display_update_time: + return False + + self.last_display_update_time = current_mtime + + content = self.read_file_content(self.display_update_file) + if content == "1": + logger.info(f"检测到 {self.display_update_file} 文件内容为1,立即进行分辨率设置") + + # 将文件内容改为0 + try: + with open(self.display_update_file, 'w') as f: + f.write("0") + logger.info(f"已将 {self.display_update_file} 文件内容修改为0") + except Exception as e: + logger.error(f"修改display_update.txt文件失败: {e}") + + return True + + except Exception as e: + logger.error(f"检查display_update.txt文件失败: {e}") + + return False + + def execute_display_settings(self, is_first=False): + """执行 display_settings 文件夹中的所有文件 - 直接调用方法""" + # 检查目录是否存在 + if not self.settings_dir.exists(): + logger.error(f"目录 {self.settings_dir} 不存在") + return False + + # 获取目录中的所有文件 + setting_files = list(self.settings_dir.glob("*")) + if not setting_files: + logger.warning(f"目录 {self.settings_dir} 中没有文件") + return True + + success_count = 0 + total_count = len(setting_files) + skipped_count = 0 + + for file_path in setting_files: + if file_path.is_file(): + try: + logger.info(f"处理显示器配置文件: {file_path}") + + # 直接调用DisplayChecker的方法,而不是通过子进程 + result = self.display_checker.check_and_configure(str(file_path), is_first) + if result: + logger.info(f"成功处理: {file_path}") + success_count += 1 + else: + # 检查是否是跳过(显示器未连接) + if not self.display_checker.is_display_connected(): + logger.warning(f"跳过未连接的显示器: {file_path}") + skipped_count += 1 + else: + logger.error(f"处理失败: {file_path}") + + except Exception as e: + logger.error(f"处理异常: {file_path}, 异常: {e}") + + logger.info(f"执行完成: 成功 {success_count}, 跳过 {skipped_count}, 总计 {total_count} 个文件") + + # 记录操作时间并更新文件修改时间 + self.last_operation_time = time.time() + + # 等待15秒后更新文件修改时间,防止重复触发 + logger.info(f"等待 {self.cooldown_period} 秒后更新文件修改时间...") + time.sleep(self.cooldown_period) + + if self.running: + self.update_file_modification_time() + + return success_count > 0 # 只要有一个成功就返回True + + def initial_resolution_check(self): + """初始分辨率检查 - 检查十次,每次间隔15秒""" + logger.info("开始初始分辨率检查流程...") + + # 检查目录是否存在 + if not self.settings_dir.exists(): + logger.error(f"目录 {self.settings_dir} 不存在") + return False + + # 获取目录中的所有文件 + setting_files = list(self.settings_dir.glob("*")) + if not setting_files: + logger.warning(f"目录 {self.settings_dir} 中没有文件") + return True + + # 进行十次检查,每次间隔15秒 + max_attempts = 10 + check_interval = 15 # 秒 + + for attempt in range(1, max_attempts + 1): + if not self.running: + logger.info("程序正在退出,终止初始分辨率检查") + return False + + logger.info(f"初始分辨率检查 - 第 {attempt}/{max_attempts} 次") + + success_count = 0 + total_count = len(setting_files) + need_adjustment = False + + for file_path in setting_files: + if not self.running: + break + + if file_path.is_file(): + try: + logger.info(f"检查显示器配置文件: {file_path}") + + # 为每个文件创建新的DisplayChecker实例 + display_checker = DisplayChecker() + + # 解析配置文件 + if not display_checker.parse_config_file(str(file_path)): + logger.error(f"解析配置文件失败: {file_path}") + continue + + # 检查显示器是否连接 + if not display_checker.is_display_connected(): + logger.warning(f"显示器 {display_checker.display_name} 未连接,跳过检查") + continue + + # 获取当前设置 + current_settings = display_checker.get_current_display_settings() + if not current_settings: + logger.warning(f"无法获取显示器 {display_checker.display_name} 的当前设置") + need_adjustment = True + continue + + # 比较设置 + match, message = display_checker.compare_settings() + + if match: + logger.info(f"✓ 显示器 {display_checker.display_name} 设置匹配: {message}") + success_count += 1 + else: + logger.info(f"✗ 显示器 {display_checker.display_name} 设置不匹配: {message}") + need_adjustment = True + + # 立即进行配置 + logger.info(f"立即配置显示器 {display_checker.display_name}...") + if display_checker.configure_display(): + logger.info(f"显示器 {display_checker.display_name} 配置成功") + success_count += 1 + else: + logger.error(f"显示器 {display_checker.display_name} 配置失败") + + except Exception as e: + logger.error(f"处理异常: {file_path}, 异常: {e}") + + # 检查是否所有显示器都配置正确 + if success_count == total_count: + logger.info(f"✓ 第 {attempt} 次检查完成: 所有显示器配置正确") + break + else: + logger.info(f"第 {attempt} 次检查完成: {success_count}/{total_count} 个显示器配置正确") + + # 如果不是最后一次检查,等待间隔时间 + if attempt < max_attempts: + if need_adjustment: + logger.info(f"等待 {check_interval} 秒后进行下一次检查...") + for i in range(check_interval): + if not self.running: + break + time.sleep(1) + + logger.info("初始分辨率检查流程完成") + return True + + def monitor(self): + """主监控循环""" + logger.info("启动文件监控脚本...") + + # 检查os-version文件 + self.check_os_version() + + logger.info("进行首次分辨率设置") + self.execute_display_settings(is_first=True) + time.sleep(10) + + # 等待screen文件生成 + if not self.wait_for_screen_file(): + return + + # 在screen.txt生成后,进行初始分辨率检查 + logger.info("screen.txt文件已生成,开始初始分辨率检查...") + self.initial_resolution_check() + + logger.info("开始监控文件变化...") + + while self.running: + try: + # 检查display_update.txt文件 + if self.check_display_update_file(): + logger.info("检测到display_update.txt触发,立即执行分辨率设置...") + self.execute_display_settings() + logger.info("display_update.txt触发操作完成,继续监控...") + + # 检查screen.txt文件是否被修改 + if self.check_file_modified(): + logger.info("检测到screen.txt文件内容变化,执行display_settings中的文件...") + self.execute_display_settings() + logger.info("screen.txt变化操作完成,继续监控...") + + # 等待1秒后继续检查 + time.sleep(1) + + except KeyboardInterrupt: + logger.info("用户中断监控") + break + except Exception as e: + logger.error(f"监控过程中发生异常: {e}") + time.sleep(10) + + logger.info("监控脚本已停止") + + +def main(): + """主函数""" + monitor = FileMonitor() + monitor.monitor() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opt/ktouch/docs/check_save.md b/opt/ktouch/docs/check_save.md new file mode 100644 index 0000000..dff4d2a --- /dev/null +++ b/opt/ktouch/docs/check_save.md @@ -0,0 +1,53 @@ +# check_save.c - 保存确认对话框 + +## 概述 +这是一个GTK+图形界面程序,用于在多显示器环境下显示保存确认对话框。用户可以在任意显示器上点击"保存"或"取消"按钮,程序会返回相应的退出代码供调用方判断。 + +## 程序结构 + +### 数据结构 +```c +typedef struct { + GtkWidget *window; // GTK窗口指针 + int monitor_num; // 显示器编号 + int x, y; // 显示器位置 + int width, height; // 显示器分辨率 + char *name; // 显示器名称/接口名 +} MonitorInfo; +``` + +### 全局变量 +- `monitors`: 显示器信息数组 +- `monitor_count`: 显示器数量 +- `app`: GTK应用程序实例 +- `exit_code`: 退出代码(默认1=取消) + +## 工作原理 + +1. **初始化阶段**: 检查DISPLAY环境变量,创建GTK应用 +2. **显示器枚举**: 使用`gdk_screen_get_n_monitors()`获取所有显示器信息 +3. **窗口创建**: 为每个显示器创建一个400x200的居中确认窗口 +4. **用户交互**: + - 点击"保存" → 打印"0",退出代码=0 + - 点击"取消并退出" → 打印"1",退出代码=1 + - 关闭窗口 → 打印"1",退出代码=1 + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `activate()` | 应用启动回调,枚举显示器并创建窗口 | +| `create_monitor_window()` | 为指定显示器创建确认窗口 | +| `on_save_clicked()` | 保存按钮回调 | +| `on_cancel_clicked()` | 取消按钮回调 | +| `show_error_dialog()` | 显示错误对话框 | + +## 调用方式 +```bash +./check_save +# 返回值: 0=保存, 1=取消 +``` + +## 依赖 +- GTK+ 3.x +- GDK (含X11后端) diff --git a/opt/ktouch/docs/demo.md b/opt/ktouch/docs/demo.md new file mode 100644 index 0000000..86f9a02 --- /dev/null +++ b/opt/ktouch/docs/demo.md @@ -0,0 +1,53 @@ +# demo.c - 多屏幕信息窗口演示程序 + +## 概述 +一个基于GTK+和Xrandr的多屏幕信息演示程序。在所有连接的显示器上依次创建信息窗口,显示屏幕分辨率、位置等信息,支持多轮测试和窗口位置校验。 + +## 程序结构 + +### 全局变量 +| 变量 | 类型 | 说明 | +|------|------|------| +| `info_windows` | `GtkWidget**` | 各屏幕信息窗口指针数组 | +| `total_screens` | `int` | 连接的显示器总数 | +| `screen_widths/heights` | `int*` | 各屏幕宽高 | +| `screen_x_offsets/y_offsets` | `int*` | 各屏幕坐标偏移 | +| `screen_names` | `char**` | 屏幕接口名(如HDMI-1) | +| `current_test_round` | `int` | 当前测试轮次 | +| `total_test_rounds` | `int` | 总测试轮次 | +| `win_time_delay` | `int` | 窗口创建间隔(ms),默认50 | + +## 工作原理 + +1. **屏幕信息获取**: 使用Xrandr扩展查询所有已连接的输出设备及其CRTC信息 +2. **窗口创建**: 每个屏幕创建一个比屏幕尺寸小20像素的窗口,偏移(10,10) +3. **定时创建**: 窗口按`win_time_delay`毫秒间隔依次创建 +4. **空格键控制**: 用户按空格键关闭所有窗口,进入下一轮测试 +5. **窗口校验**: 每轮结束后校验窗口位置和大小(允许±5像素误差) +6. **日志记录**: 所有操作记录到`screen_demo.log`文件 + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `get_screen_info()` | 通过Xrandr获取所有屏幕信息 | +| `create_info_window()` | 创建单个屏幕的信息窗口 | +| `create_next_window()` | 定时回调,依次创建窗口 | +| `verify_windows()` | 校验窗口位置和大小 | +| `start_next_test_round()` | 开始新一轮测试 | +| `on_key_press()` | 空格键事件处理 | + +## 窗口布局 +``` +窗口位置: (screen_x + 10, screen_y + 10) +窗口大小: (screen_width - 20) x (screen_height - 20) +``` + +## 调用方式 +```bash +./demo +``` + +## 依赖 +- GTK+ 3.x +- X11 + Xrandr扩展 diff --git a/opt/ktouch/docs/gdk_test.md b/opt/ktouch/docs/gdk_test.md new file mode 100644 index 0000000..3092636 --- /dev/null +++ b/opt/ktouch/docs/gdk_test.md @@ -0,0 +1,26 @@ +# gdk_test.c - GDK类型测试 + +## 概述 +一个极简的编译测试程序,用于验证`GdkMonitor`类型在当前GTK/GDK版本中是否可用。 + +## 代码 +```c +#include +#include + +int main(int argc, char *argv[]) { + gtk_init(&argc, &argv); + GdkMonitor *monitor; // 仅测试该类型是否可识别 + return 0; +} +``` + +## 用途 +该文件不包含实际功能逻辑,仅用于: +- 验证编译环境中GDK头文件是否包含`GdkMonitor`类型定义 +- 确认GTK版本是否支持该类型(GTK 3.22+引入) + +## 编译测试 +```bash +gcc gdk_test.c -o gdk_test $(pkg-config --cflags --libs gtk+-3.0) +``` diff --git a/opt/ktouch/docs/screen_binder.md b/opt/ktouch/docs/screen_binder.md new file mode 100644 index 0000000..99805f3 --- /dev/null +++ b/opt/ktouch/docs/screen_binder.md @@ -0,0 +1,89 @@ +# screen_binder.c - 触摸屏与显示器绑定程序 + +## 概述 +核心模块之一,用于将触摸屏设备与显示器进行一一绑定。程序在每个显示器上依次弹出校准窗口,用户点击触摸屏后,程序记录触摸设备与当前显示器的对应关系,最终保存到`touch_dis_table.txt`文件。 + +## 程序结构 + +### 数据结构 +```c +// 屏幕信息 +int *screen_widths, *screen_heights; // 屏幕宽高 +int *screen_x_offsets, *screen_y_offsets; // 屏幕坐标 +char **screen_names; // 屏幕接口名 + +// 触摸绑定信息 +int *touch_bindings; // 每个屏幕绑定的触摸设备ID +char **touch_names; // 触摸设备名称 +char **touch_paths; // 触摸设备路径 +``` + +### 线程模型 +| 线程 | 功能 | +|------|------| +| 主线程 | GTK事件循环,窗口管理 | +| `touch_thread` | 触摸事件监听线程 | +| `enter_thread` | 全局回车键监听线程(X11) | + +### 原子变量(线程安全) +```c +_Atomic int global_enter_pressed; // 全局回车键按下标志 +_Atomic int program_active; // 程序活动状态 +_Atomic int calibration_active; // 校准进行中标志 +``` + +## 工作原理 + +1. **屏幕枚举**: 通过Xrandr获取所有已连接显示器 +2. **触摸初始化**: 初始化触摸监听器,启动触摸事件监听线程 +3. **全局回车监听**: 启动独立线程监听全局回车键(用于跳过非触摸屏) +4. **信息窗口创建**: 依次为每个非当前校准屏幕创建提示窗口 +5. **校准流程**: + - 在当前屏幕创建全屏校准窗口 + - 等待用户点击触摸屏(60秒超时) + - 记录触摸设备ID、名称、路径 + - 用户按回车键可跳过当前屏幕 +6. **保存结果**: 将绑定关系保存到`touch_dis_table.txt` + +## 校准窗口交互 +``` +┌─────────────────────────────────────┐ +│ 请在触摸屏上点击此屏幕。 │ +│ │ +│ 屏幕接口: HDMI-1 │ +│ │ +│ 如果此屏幕不是触摸屏, │ +│ 那么请敲击回车或者等待60秒。 │ +└─────────────────────────────────────┘ +``` + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `get_screen_info()` | Xrandr获取屏幕信息 | +| `init_touch_listener()` | 初始化触摸监听 | +| `touch_callback()` | 触摸事件回调,记录绑定 | +| `global_enter_listener_thread()` | X11全局回车键监听 | +| `show_next_screen()` | 显示下一个校准屏幕 | +| `save_mapping_table()` | 保存绑定关系到文件 | +| `create_calibration_window()` | 创建校准窗口 | +| `create_info_window()` | 创建信息提示窗口 | + +## 输出格式 +文件`touch_dis_table.txt`格式: +``` +屏幕接口名|触摸设备名|触摸设备ID|设备路径 +HDMI-1|USB Touchscreen|12345|/dev/input/event5 +``` + +## 调用方式 +```bash +sudo ./screen_binder # 需要sudo权限访问触摸设备 +``` + +## 依赖 +- GTK+ 3.x +- X11 + Xrandr扩展 +- touch_listen模块(触摸事件监听) +- 需要root权限访问`/dev/input/`设备 diff --git a/opt/ktouch/docs/screen_ds.md b/opt/ktouch/docs/screen_ds.md new file mode 100644 index 0000000..85052d5 --- /dev/null +++ b/opt/ktouch/docs/screen_ds.md @@ -0,0 +1,70 @@ +# screen_ds.c - 显示器信息持续监控程序 + +## 概述 +一个基于Xrandr的显示器状态持续监控程序。持续监听显示器的连接/断开事件,当检测到变化时(防抖3秒后)输出所有显示器的当前信息。 + +## 程序结构 + +### 数据结构 +```c +typedef struct { + char* output_name; // 输出接口名(如HDMI-1) + int width, height; // 分辨率 + int x, y; // 位置坐标 + int rotation; // 旋转方向 +} MonitorInfo; + +typedef struct { + MonitorInfo* monitors; // 显示器数组 + int count; // 显示器数量 +} MonitorList; +``` + +### 旋转方向编码 +| 值 | 字母 | 说明 | +|----|------|------| +| RR_Rotate_0 | N | 正常方向 | +| RR_Rotate_90 | L | 左旋转90度 | +| RR_Rotate_180 | I | 翻转180度 | +| RR_Rotate_270 | R | 右旋转90度 | + +## 工作原理 + +1. **初始化**: 打开X显示连接,注册RandR事件监听 +2. **首次输出**: 获取并输出当前所有显示器信息 +3. **事件循环**: + - 监听`RRScreenChangeNotify`和`RRNotify`事件 + - 检测到事件后设置`need_update`标志 + - 防抖3秒后(`DEBOUNCE_TIME`)重新获取并输出显示器信息 +4. **无事件时**: 休眠100ms减少CPU占用 + +## 输出格式 +``` +HDMI-1|1920x1080|0x0|N +DP-2|1920x1080|1920x0|N +``` +每行格式: `接口名|分辨率|位置|旋转方向` + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `get_monitor_info()` | 获取所有连接的显示器信息 | +| `output_monitor_info()` | 输出显示器信息到文件或stdout | +| `rotation_to_char()` | 旋转值转字母表示 | +| `current_timestamp()` | 获取毫秒级时间戳 | + +## 调用方式 +```bash +./screen_ds [输出文件路径] +# 例: ./screen_ds /tmp/screen.txt +# 不指定参数则输出到标准输出 +``` + +## 特性 +- 3秒防抖:避免频繁热插拔导致的重复输出 +- 持续运行,直到Ctrl+C退出 +- 支持输出到文件或标准输出 + +## 依赖 +- X11 + Xrandr扩展 diff --git a/opt/ktouch/docs/screen_ds_once.md b/opt/ktouch/docs/screen_ds_once.md new file mode 100644 index 0000000..5d4df46 --- /dev/null +++ b/opt/ktouch/docs/screen_ds_once.md @@ -0,0 +1,50 @@ +# screen_ds_once.c - 显示器信息单次查询程序 + +## 概述 +screen_ds.c的简化版本,仅执行一次显示器信息查询并输出结果后退出。适用于需要快速获取当前显示器配置的场景。 + +## 与screen_ds.c的区别 +| 特性 | screen_ds | screen_ds_once | +|------|-----------|----------------| +| 执行模式 | 持续监控 | 单次查询 | +| 事件监听 | 有 | 无 | +| 退出方式 | Ctrl+C | 自动退出 | + +## 工作原理 + +1. 打开X显示连接 +2. 查询RandR扩展是否可用 +3. 获取所有已连接显示器信息 +4. 输出到文件或标准输出 +5. 释放资源并退出 + +## 数据结构 +与screen_ds.c相同: +```c +typedef struct { + char* output_name; + int width, height; + int x, y; + int rotation; +} MonitorInfo; +``` + +## 输出格式 +``` +HDMI-1|1920x1080|0x0|N +``` + +## 关键函数 +| 函数 | 功能 | +|------|------| +| `get_monitor_info()` | 获取显示器信息 | +| `output_monitor_info()` | 输出信息 | +| `rotation_to_char()` | 旋转方向编码转换 | + +## 调用方式 +```bash +./screen_ds_once [输出文件路径] +``` + +## 依赖 +- X11 + Xrandr扩展 diff --git a/opt/ktouch/docs/touch_ds.md b/opt/ktouch/docs/touch_ds.md new file mode 100644 index 0000000..172a563 --- /dev/null +++ b/opt/ktouch/docs/touch_ds.md @@ -0,0 +1,73 @@ +# touch_ds.c - 触摸屏坐标矩阵监控程序 + +## 概述 +持续监控所有触摸屏设备的坐标转换矩阵(Coordinate Transformation Matrix)。当检测到某个触摸设备的矩阵发生变化时,设置flag文件通知外部程序。 + +## 程序结构 + +### 数据结构 +```c +typedef struct { + XID deviceid; // XInput设备ID + char name[100]; // 设备名称 + char node[100]; // 设备节点路径 + char vid_pid[20]; // VID:PID + char matrix[120]; // 坐标转换矩阵字符串 +} TouchscreenInfo; +``` + +### 配置常量 +```c +int watch_time_delay = 30; // 监控间隔(秒) +``` + +## 工作原理 + +1. **读取配置**: 从触摸屏信息文件读取设备列表(含期望的矩阵值) +2. **X11连接**: 打开X显示连接,检查XInput扩展 +3. **主循环**: + - 检查flag文件是否为1(暂停检测) + - 重新读取触摸屏信息文件 + - 对每个设备获取当前矩阵值 + - 比较矩阵是否与配置值不同 + - 若变化,设置flag文件为1 + - 等待30秒后继续 + +### 矩阵获取流程 +``` +设备 → XInput属性查询 → "Coordinate Transformation Matrix" + → 或 "libinput Calibration Matrix" + → 返回3x3浮点矩阵 +``` + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `get_device_matrix_property()` | 获取设备的3x3矩阵属性 | +| `get_matrix_string()` | 获取矩阵的字符串表示 | +| `device_exists()` | 检查设备是否仍然存在 | +| `is_touchscreen_device()` | 通过udev判断是否为触摸屏 | +| `read_touchscreens_from_file()` | 读取触摸屏配置文件 | +| `is_flag_set()` / `set_flag()` | flag文件读写 | + +## 输入文件格式 +触摸屏信息文件每行格式: +``` +设备ID|设备名|设备节点|VID:PID|矩阵值 +12345|USB Touchscreen|/dev/input/event5|046d:c52b|1.000000,0.000000,... +``` + +## flag文件机制 +- flag文件值为`0`:正常监控 +- flag文件值为`1`:暂停监控,等待外部恢复为`0` + +## 调用方式 +```bash +./touch_ds <触摸屏信息文件> [flag文件路径] +# 例: ./touch_ds /tmp/touch_info.txt /tmp/touch_matrix.flag +``` + +## 依赖 +- X11 + XInput2扩展 +- libudev diff --git a/opt/ktouch/docs/touch_listen.md b/opt/ktouch/docs/touch_listen.md new file mode 100644 index 0000000..02f1174 --- /dev/null +++ b/opt/ktouch/docs/touch_listen.md @@ -0,0 +1,99 @@ +# touch_listen.c / touch_listen.h - 触摸事件监听库 + +## 概述 +一个触摸屏事件监听库,通过直接读取Linux输入设备(`/dev/input/event*`)获取触摸事件。支持多点触控,提供回调函数接口供上层模块使用。 + +## 数据结构 + +### touch_device (设备信息) +```c +struct touch_device { + int fd; // 文件描述符 + char name[256]; // 设备名称 + char path[256]; // 设备路径 + int min_x, max_x; // X轴范围 + int min_y, max_y; // Y轴范围 +}; +``` + +### touch_event (触摸事件) +```c +struct touch_event { + int device_id; // 设备标识(路径哈希值) + int x, y; // 坐标 + int pressure; // 压力值 + int touch_id; // 触摸点ID(多点触控) + int event_type; // 事件类型: 0=按下, 1=移动, 2=释放 + char device_name[256]; // 设备名称 + char device_path[256]; // 设备路径 + struct timespec timestamp; // 时间戳 +}; +``` + +### 回调函数类型 +```c +typedef void (*touch_event_callback)(struct touch_event event); +``` + +## 工作原理 + +### 设备发现 +1. 遍历`/dev/input/`目录下所有`event*`设备 +2. 通过ioctl检查设备是否支持`EV_ABS`和`EV_KEY` +3. 进一步检查是否支持`ABS_MT_POSITION_X`(多点)或`ABS_X`(单点) +4. 获取设备名称和坐标范围 + +### 事件监听 +1. 使用`select()`多路复用监听所有设备文件描述符 +2. 读取`input_event`结构体 +3. 处理事件类型: + - `EV_ABS`: 处理坐标、压力、触摸点ID + - `EV_KEY`: 处理`BTN_TOUCH`按键事件 + - `EV_SYN`: 同步事件,触发回调 +4. 通过回调函数通知上层 + +### 事件类型映射 +| Linux事件 | 触摸事件类型 | 说明 | +|-----------|-------------|------| +| ABS_MT_TRACKING_ID != -1 | 0 (按下) | 新触摸点 | +| ABS_MT_TRACKING_ID == -1 | 2 (释放) | 触摸释放 | +| BTN_TOUCH = 1 | 0 (按下) | 触摸开始 | +| BTN_TOUCH = 0 | 2 (释放) | 触摸结束 | +| SYN_REPORT | 1 (移动) | 坐标更新 | + +## API接口 + +| 函数 | 功能 | +|------|------| +| `init_touch_listener(callback)` | 初始化监听器,注册回调 | +| `listen_touch_events()` | 开始监听(阻塞) | +| `stop_touch_listener()` | 停止监听 | +| `cleanup_touch_listener()` | 清理资源 | +| `get_active_touch_devices()` | 获取活跃设备列表 | + +## 调用示例 +```c +#include "touch_listen.h" + +void my_callback(struct touch_event event) { + printf("设备 %s: %s at (%d,%d)\n", + event.device_name, + event.event_type == 0 ? "按下" : + event.event_type == 1 ? "移动" : "释放", + event.x, event.y); +} + +int main() { + init_touch_listener(my_callback); + listen_touch_events(); // 阻塞 + cleanup_touch_listener(); + return 0; +} +``` + +## 使用者 +- `screen_binder.c` - 触摸屏绑定程序 + +## 依赖 +- Linux输入子系统(`/dev/input/event*`) +- 需要root权限访问输入设备 diff --git a/opt/ktouch/docs/touch_set.md b/opt/ktouch/docs/touch_set.md new file mode 100644 index 0000000..fe2cf2a --- /dev/null +++ b/opt/ktouch/docs/touch_set.md @@ -0,0 +1,152 @@ +# touch_set.c - 触摸屏坐标转换矩阵设置程序 + +## 概述 +核心模块之一,负责根据配置文件和屏幕信息,自动计算并设置每个触摸屏设备的坐标转换矩阵(Coordinate Transformation Matrix),实现触摸屏与显示器的正确映射。 + +## 程序结构 + +### 数据结构 +```c +// 设备信息 +typedef struct { + int device_id; // XInput设备ID + char name[256]; // 设备名称 + char device_node[256]; // 设备节点路径 + char vid[10]; // 供应商ID + char pid[10]; // 产品ID + char usb_path[100]; // 物理路径 + int is_touchscreen; // 是否为触摸屏 + float matrix[9]; // 3x3坐标转换矩阵 + int matched; // 是否已匹配 +} InputDeviceInfo; + +// 配置项 +typedef struct { + char display_name[50]; // 显示器名称 + char touchscreen_name[50]; // 触摸屏名称 + char vid[10]; // 供应商ID + char pid[10]; // 产品ID + char usb_path[100]; // USB路径 + int matched; // 是否已匹配 +} TouchConfig; + +// 屏幕信息 +typedef struct { + char name[50]; // 屏幕名称 + int width, height; // 分辨率 + int x, y; // 位置 + char rotation; // 旋转: N/L/R/I +} ScreenInfo; +``` + +## 工作原理 + +### 整体流程 +``` +读取配置文件(/opt/ktouch/config) + ↓ +读取屏幕信息(/tmp/ktouch/screen.txt) + ↓ +计算虚拟桌面大小 + ↓ +获取所有触摸设备(XInput + udev) + ↓ +多轮匹配(严格→宽松) + ↓ +计算坐标转换矩阵 + ↓ +通过XInput设置矩阵 + ↓ +保存结果到输出文件 +``` + +### 矩阵计算原理 + +坐标转换矩阵将触摸屏的原始坐标映射到虚拟桌面坐标系: + +**正常方向(N)**: +``` +| sx 0 dx | +| 0 sy dy | +| 0 0 1 | +``` + +**左旋转(L)**: +``` +| 0 -sx sx+dx | +| sy 0 dy | +| 0 0 1 | +``` + +**右旋转(R)**: +``` +| 0 sx dx | +| -sy 0 sy+dy | +| 0 0 1 | +``` + +**翻转(I)**: +``` +| -sx 0 sx+dx | +| 0 -sy sy+dy | +| 0 0 1 | +``` + +其中: +- `sx = 屏幕宽度 / 虚拟桌面宽度` +- `sy = 屏幕高度 / 虚拟桌面高度` +- `dx = 屏幕X偏移 / 虚拟桌面宽度` +- `dy = 屏幕Y偏移 / 虚拟桌面高度` + +### 设备匹配策略 + +**第一轮(严格匹配)**: +1. 设备名完全匹配(去除空格后比较) +2. 若多个匹配,按VID:PID筛选 +3. 若仍多个,按USB路径筛选 + +**第二轮及以后(宽松匹配)**: +1. 仅按VID:PID匹配 +2. 若多个匹配,返回失败(-1) + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `get_input_devices()` | 通过XInput+udev获取所有触摸设备 | +| `read_config()` | 读取触摸屏配置文件 | +| `read_screen_info()` | 读取屏幕信息文件 | +| `find_matching_device()` | 多策略设备匹配 | +| `calculate_virtual_desktop()` | 计算虚拟桌面总尺寸 | +| `calculate_ctm()` | 计算坐标转换矩阵 | +| `set_ctm()` | 通过XInput设置矩阵属性 | +| `devnode_to_syspath()` | 设备节点转sysfs路径 | + +## 输入文件格式 + +### 配置文件(/opt/ktouch/config) +``` +显示器名|触摸屏名|VID|PID|USB路径 +HDMI-1|USB Touchscreen|046d|c52b|usb-0000:00:14.0-1 +``` + +### 屏幕信息文件(/tmp/ktouch/screen.txt) +``` +HDMI-1|1920x1080|0x0|N +DP-2|1920x1080|1920x0|N +``` + +### 输出文件格式 +``` +设备ID|设备名|设备节点|VID:PID|矩阵值 +12345|USB Touchscreen|/dev/input/event5|046d:c52b|1.000000, 0.000000, ... +``` + +## 调用方式 +```bash +./touch_set [输出文件路径] +``` + +## 依赖 +- X11 + XInput2扩展 +- libudev diff --git a/opt/ktouch/docs/usb_ds.md b/opt/ktouch/docs/usb_ds.md new file mode 100644 index 0000000..5b6c8a6 --- /dev/null +++ b/opt/ktouch/docs/usb_ds.md @@ -0,0 +1,77 @@ +# usb_ds.c - USB触摸屏设备热插拔监控程序 + +## 概述 +通过udev监控USB设备的热插拔事件,当检测到新的触摸屏设备插入时,通过控制文件通知外部程序。支持轮询间隔配置和控制文件暂停机制。 + +## 程序结构 + +### 配置常量 +```c +#define DEFAULT_POLL_INTERVAL 5 // 默认轮询间隔(秒) +#define MAX_RETRY_ATTEMPTS 3 // 最大重试次数 +#define RETRY_DELAY 1 // 重试延迟(秒) +``` + +### 控制变量 +```c +volatile sig_atomic_t keep_running = 1; // 运行标志 +``` + +## 工作原理 + +### 监控流程 +``` +创建udev监控对象 + ↓ +过滤USB和input子系统事件 + ↓ +主循环: + ├── 检查控制文件状态 + │ └── 若非0,跳过本轮 + ├── select等待事件(超时=轮询间隔) + ├── 有事件: + │ ├── 获取设备信息 + │ ├── 检查是否为"add"动作 + │ ├── 检查是否为指针设备 + │ └── 检查是否为触摸屏 + └── 更新控制文件状态 +``` + +### 触摸屏判断 +```c +int is_touchscreen(struct udev_device *dev) { + // 1. 检查ID_INPUT_TOUCHSCREEN属性 + // 2. 检查设备名称是否包含"touch"/"touchscreen"/"tablet" +} +``` + +### 控制文件机制 +- `0`: 正常监控状态 +- `1`: 检测到触摸屏,等待外部处理后恢复为`0` + +### 文件操作重试 +所有文件读写操作最多重试3次,每次间隔1秒。 + +## 关键函数 + +| 函数 | 功能 | +|------|------| +| `monitor_usb_devices()` | 主监控循环 | +| `is_touchscreen()` | 判断设备是否为触摸屏 | +| `read_file_content_with_retry()` | 带重试的文件读取 | +| `write_file_content_with_retry()` | 带重试的文件写入 | +| `sigint_handler()` | SIGINT信号处理 | + +## 调用方式 +```bash +./usb_ds [控制文件路径] [轮询间隔秒数] +# 例: ./usb_ds /tmp/usb_monitor.ctl 5 +# 不带参数: 无控制文件,5秒轮询 +``` + +## 使用场景 +当USB触摸屏热插拔时,通过控制文件通知主程序重新配置触摸映射。 + +## 依赖 +- libudev +- POSIX信号处理 diff --git a/opt/ktouch/forest-light.tcl b/opt/ktouch/forest-light.tcl new file mode 100644 index 0000000..cf9e2cd --- /dev/null +++ b/opt/ktouch/forest-light.tcl @@ -0,0 +1,541 @@ +# Copyright (c) 2021 rdbende + +# The Forest theme is a beautiful and modern ttk theme inspired by Excel. + +package require Tk 8.6 + +namespace eval ttk::theme::forest-light { + + variable version 1.0 + package provide ttk::theme::forest-light $version + variable colors + array set colors { + -fg "#313131" + -bg "#ffffff" + -disabledfg "#595959" + -disabledbg "#ffffff" + -selectfg "#ffffff" + -selectbg "#217346" + } + + proc LoadImages {imgdir} { + variable I + foreach file [glob -directory $imgdir *.png] { + set img [file tail [file rootname $file]] + set I($img) [image create photo -file $file -format png] + } + } + + LoadImages [file join [file dirname [info script]] forest-light] + + # Settings + ttk::style theme create forest-light -parent default -settings { + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertwidth 1 \ + -insertcolor $colors(-fg) \ + -fieldbackground $colors(-selectbg) \ + -font {TkDefaultFont 10} \ + -borderwidth 1 \ + -relief flat + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + tk_setPalette background [ttk::style lookup . -background] \ + foreground [ttk::style lookup . -foreground] \ + highlightColor [ttk::style lookup . -focuscolor] \ + selectBackground [ttk::style lookup . -selectbackground] \ + selectForeground [ttk::style lookup . -selectforeground] \ + activeBackground [ttk::style lookup . -selectbackground] \ + activeForeground [ttk::style lookup . -selectforeground] + + option add *font [ttk::style lookup . -font] + + + # Layouts + ttk::style layout TButton { + Button.button -children { + Button.padding -children { + Button.label -side left -expand true + } + } + } + + ttk::style layout Toolbutton { + Toolbutton.button -children { + Toolbutton.padding -children { + Toolbutton.label -side left -expand true + } + } + } + + ttk::style layout TMenubutton { + Menubutton.button -children { + Menubutton.padding -children { + Menubutton.indicator -side right + Menubutton.label -side right -expand true + } + } + } + + ttk::style layout TOptionMenu { + OptionMenu.button -children { + OptionMenu.padding -children { + OptionMenu.indicator -side right + OptionMenu.label -side right -expand true + } + } + } + + ttk::style layout Accent.TButton { + AccentButton.button -children { + AccentButton.padding -children { + AccentButton.label -side left -expand true + } + } + } + + ttk::style layout TCheckbutton { + Checkbutton.button -children { + Checkbutton.padding -children { + Checkbutton.indicator -side left + Checkbutton.label -side right -expand true + } + } + } + + ttk::style layout Switch { + Switch.button -children { + Switch.padding -children { + Switch.indicator -side left + Switch.label -side right -expand true + } + } + } + + ttk::style layout ToggleButton { + ToggleButton.button -children { + ToggleButton.padding -children { + ToggleButton.label -side left -expand true + } + } + } + + ttk::style layout TRadiobutton { + Radiobutton.button -children { + Radiobutton.padding -children { + Radiobutton.indicator -side left + Radiobutton.label -side right -expand true + } + } + } + + ttk::style layout Vertical.TScrollbar { + Vertical.Scrollbar.trough -sticky ns -children { + Vertical.Scrollbar.thumb -expand true + } + } + + ttk::style layout Horizontal.TScrollbar { + Horizontal.Scrollbar.trough -sticky ew -children { + Horizontal.Scrollbar.thumb -expand true + } + } + + ttk::style layout TCombobox { + Combobox.field -sticky nswe -children { + Combobox.padding -expand true -sticky nswe -children { + Combobox.textarea -sticky nswe + } + } + Combobox.button -side right -sticky ns -children { + Combobox.arrow -sticky nsew + } + } + + ttk::style layout TSpinbox { + Spinbox.field -sticky nsew -children { + Spinbox.padding -expand true -sticky nswe -children { + Spinbox.textarea -sticky nsew + } + + } + null -side right -sticky nsew -children { + Spinbox.uparrow -side right -sticky nsew -children { + Spinbox.symuparrow + } + Spinbox.downarrow -side left -sticky nsew -children { + Spinbox.symdownarrow + } + } + } + + ttk::style layout Horizontal.TSeparator { + Horizontal.separator -sticky nswe + } + + ttk::style layout Vertical.TSeparator { + Vertical.separator -sticky nswe + } + + ttk::style layout Card { + Card.field { + Card.padding -expand 1 + } + } + + ttk::style layout TLabelframe { + Labelframe.border { + Labelframe.padding -expand 1 -children { + Labelframe.label -side left + } + } + } + + ttk::style layout TNotebook { + Notebook.border -children { + TNotebook.Tab -expand 1 -side top + Notebook.client -sticky nsew + } + } + + ttk::style layout TNotebook.Tab { + Notebook.tab -children { + Notebook.padding -side top -children { + Notebook.label + } + } + } + + ttk::style layout Treeview.Item { + Treeitem.padding -sticky nswe -children { + Treeitem.indicator -side left -sticky {} + Treeitem.image -side left -sticky {} + Treeitem.text -side left -sticky {} + } + } + + + # Elements + + # Button + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create Button.button image \ + [list $I(rect-basic) \ + {selected disabled} $I(rect-basic) \ + disabled $I(rect-basic) \ + selected $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(rect-hover) \ + ] -border 4 -sticky nsew + + # Toolbutton + ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create Toolbutton.button image \ + [list $I(empty) \ + {selected disabled} $I(empty) \ + disabled $I(empty) \ + selected $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(rect-basic) \ + ] -border 4 -sticky nsew + + # Menubutton + ttk::style configure TMenubutton -padding {8 4 4 4} + + ttk::style element create Menubutton.button image \ + [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(rect-hover) \ + ] -border 4 -sticky nsew + + ttk::style element create Menubutton.indicator image \ + [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # OptionMenu + ttk::style configure TOptionMenu -padding {8 4 4 4} + + ttk::style element create OptionMenu.button image \ + [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(rect-hover) \ + ] -border 4 -sticky nsew + + ttk::style element create OptionMenu.indicator image \ + [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # AccentButton + ttk::style configure Accent.TButton -padding {8 4 8 4} -width -10 -anchor center -foreground #eeeeee + + ttk::style element create AccentButton.button image \ + [list $I(rect-accent) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-accent-hover) \ + selected $I(rect-accent) \ + pressed $I(rect-accent) \ + active $I(rect-accent-hover) \ + ] -border 4 -sticky nsew + + # Checkbutton + ttk::style configure TCheckbutton -padding 4 + + ttk::style element create Checkbutton.indicator image \ + [list $I(check-unsel-accent) \ + {alternate disabled} $I(check-tri-basic) \ + {selected disabled} $I(check-basic) \ + disabled $I(check-unsel-basic) \ + {pressed alternate} $I(check-tri-hover) \ + {active alternate} $I(check-tri-hover) \ + alternate $I(check-tri-accent) \ + {pressed selected} $I(check-hover) \ + {active selected} $I(check-hover) \ + selected $I(check-accent) \ + {pressed !selected} $I(check-unsel-pressed) \ + active $I(check-unsel-hover) \ + ] -width 26 -sticky w + + # Switch + ttk::style element create Switch.indicator image \ + [list $I(off-accent) \ + {selected disabled} $I(on-basic) \ + disabled $I(off-basic) \ + {pressed selected} $I(on-accent) \ + {active selected} $I(on-hover) \ + selected $I(on-accent) \ + {pressed !selected} $I(off-accent) \ + active $I(off-hover) \ + ] -width 46 -sticky w + + # ToggleButton + ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center -foregound $colors(-fg) + + ttk::style map ToggleButton -foreground \ + [list {pressed selected} $colors(-fg) \ + {pressed !selected} #ffffff \ + selected #ffffff] + + ttk::style element create ToggleButton.button image \ + [list $I(rect-basic) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-basic) \ + {pressed selected} $I(rect-basic) \ + {active selected} $I(rect-accent-hover) \ + selected $I(rect-accent) \ + {pressed !selected} $I(rect-accent) \ + active $I(rect-hover) \ + ] -border 4 -sticky nsew + + # Radiobutton + ttk::style configure TRadiobutton -padding 4 + + ttk::style element create Radiobutton.indicator image \ + [list $I(radio-unsel-accent) \ + {alternate disabled} $I(radio-tri-basic) \ + {selected disabled} $I(radio-basic) \ + disabled $I(radio-unsel-basic) \ + {pressed alternate} $I(radio-tri-hover) \ + {active alternate} $I(radio-tri-hover) \ + alternate $I(radio-tri-accent) \ + {pressed selected} $I(radio-hover) \ + {active selected} $I(radio-hover) \ + selected $I(radio-accent) \ + {pressed !selected} $I(radio-unsel-pressed) \ + active $I(radio-unsel-hover) \ + ] -width 26 -sticky w + + # Scrollbar + ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Scrollbar.thumb image \ + [list $I(hor-accent) \ + disabled $I(hor-basic) \ + pressed $I(hor-hover) \ + active $I(hor-hover) \ + ] -sticky ew + + ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Scrollbar.thumb image \ + [list $I(vert-accent) \ + disabled $I(vert-basic) \ + pressed $I(vert-hover) \ + active $I(vert-hover) \ + ] -sticky ns + + # Scale + ttk::style element create Horizontal.Scale.trough image $I(scale-hor) \ + -border 5 -padding 0 + + ttk::style element create Horizontal.Scale.slider image \ + [list $I(thumb-hor-accent) \ + disabled $I(thumb-hor-basic) \ + pressed $I(thumb-hor-hover) \ + active $I(thumb-hor-hover) \ + ] -sticky {} + + ttk::style element create Vertical.Scale.trough image $I(scale-vert) \ + -border 5 -padding 0 + + ttk::style element create Vertical.Scale.slider image \ + [list $I(thumb-vert-accent) \ + disabled $I(thumb-vert-basic) \ + pressed $I(thumb-vert-hover) \ + active $I(thumb-vert-hover) \ + ] -sticky {} + + # Progressbar + ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \ + -sticky ew + + ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \ + -sticky ns + + # Entry + ttk::style element create Entry.field image \ + [list $I(border-basic) \ + {focus hover} $I(border-accent) \ + invalid $I(border-invalid) \ + disabled $I(border-basic) \ + focus $I(border-accent) \ + hover $I(border-hover) \ + ] -border 5 -padding {8} -sticky nsew + + # Combobox + ttk::style map TCombobox -selectbackground [list \ + {!focus} $colors(-selectbg) \ + {readonly hover} $colors(-selectbg) \ + {readonly focus} $colors(-selectbg) \ + ] + + ttk::style map TCombobox -selectforeground [list \ + {!focus} $colors(-selectfg) \ + {readonly hover} $colors(-selectfg) \ + {readonly focus} $colors(-selectfg) \ + ] + + ttk::style element create Combobox.field image \ + [list $I(border-basic) \ + {readonly disabled} $I(rect-basic) \ + {readonly pressed} $I(rect-basic) \ + {readonly focus hover} $I(rect-hover) \ + {readonly focus} $I(rect-hover) \ + {readonly hover} $I(rect-hover) \ + {focus hover} $I(border-accent) \ + readonly $I(rect-basic) \ + invalid $I(border-invalid) \ + disabled $I(border-basic) \ + focus $I(border-accent) \ + hover $I(border-hover) \ + ] -border 5 -padding {8 8 28 8} + + ttk::style element create Combobox.button image \ + [list $I(combo-button-basic) \ + {!readonly focus} $I(combo-button-focus) \ + {readonly focus} $I(combo-button-hover) \ + {readonly hover} $I(combo-button-hover) + ] -border 5 -padding {2 6 6 6} + + ttk::style element create Combobox.arrow image $I(down) -width 15 -sticky e + + # Spinbox + ttk::style element create Spinbox.field image \ + [list $I(border-basic) \ + invalid $I(border-invalid) \ + disabled $I(border-basic) \ + focus $I(border-accent) \ + hover $I(border-hover) \ + ] -border 5 -padding {8 8 54 8} -sticky nsew + + ttk::style element create Spinbox.uparrow image $I(spin-button-up) -border 4 -sticky nsew + + ttk::style element create Spinbox.downarrow image \ + [list $I(spin-button-down-basic) \ + focus $I(spin-button-down-focus) \ + ] -border 4 -sticky nsew + + ttk::style element create Spinbox.symuparrow image $I(up) -width 15 -sticky {} + ttk::style element create Spinbox.symdownarrow image $I(down) -width 17 -sticky {} + + # Sizegrip + ttk::style element create Sizegrip.sizegrip image $I(sizegrip) \ + -sticky nsew + + # Separator + ttk::style element create Horizontal.separator image $I(separator) + + ttk::style element create Vertical.separator image $I(separator) + + # Card + ttk::style element create Card.field image $I(card) \ + -border 10 -padding 4 -sticky nsew + + # Labelframe + ttk::style element create Labelframe.border image $I(card) \ + -border 5 -padding 4 -sticky nsew + + # Notebook + ttk::style configure TNotebook -padding 2 + + ttk::style element create Notebook.border image $I(card) -border 5 + + ttk::style element create Notebook.client image $I(notebook) -border 5 + + ttk::style element create Notebook.tab image \ + [list $I(tab-basic) \ + selected $I(tab-accent) \ + active $I(tab-hover) \ + ] -border 5 -padding {14 4} + + # Treeview + ttk::style element create Treeview.field image $I(card) \ + -border 5 + + ttk::style element create Treeheading.cell image \ + [list $I(tree-basic) \ + pressed $I(tree-pressed) + ] -border 5 -padding 6 -sticky nsew + + ttk::style element create Treeitem.indicator image \ + [list $I(right) \ + user2 $I(empty) \ + {user1 focus} $I(down-focus) \ + focus $I(right-focus) \ + user1 $I(down) \ + ] -width 17 -sticky {} + + ttk::style configure Treeview -background $colors(-bg) + ttk::style configure Treeview.Item -padding {2 0 0 0} + + ttk::style map Treeview \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] + + # Sashes + #ttk::style map TPanedwindow -background [list hover $colors(-bg)] + } +} diff --git a/opt/ktouch/forest-light/border-accent-hover.png b/opt/ktouch/forest-light/border-accent-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..72c3e600d27d2327828b76a70469e0448bc1b506 GIT binary patch literal 445 zcmV;u0Yd(XP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10ZK_k zK~y-6?bJI+f?*WK@xQmcd>R!BK@dny4OzJvMPkq>MQuSr4y}<5ZS5Le#&WR8%|K|? zT*_wM(;*wY4&G8M-Xeh@i`WQwsWj_7K|eF7lL&i`QTw7nD$P0rY7KkNP2AHamYcp;4(xq?6>TC6t{%bK9J zhq!#aCEvrO`D&SqhcIXH5C97kvnbMqtXL21|M|cU8BE;mo}&;#h)nSnf9#4xI!Ue7 n=m|1v4J=O1ak#R>=%ATT2_SQmApF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10bEH$ zK~y-6?bJIgLva+x@o!)Hu4l!-Kq7`t=pqIZBtjw{F_><>eV|zUZ`>Yky8Nd zCM&w3i7`)HN^o|4fkCIIuCfN3(NYkUlvC`64_G_iAt!S5xZ2RfrGyyXnEmyEEw?xb zE^MI@LWryTTULU8La`{y%X3kX&19jY$<6Y>G&QAFd;q}Dc44z~P*DH?002ovPDHLk FV1gMZ!~_5U literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/border-basic.png b/opt/ktouch/forest-light/border-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..1094b6d01d4f932486e8adbcbf39ae930db48708 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|*=j7sn8b-nUl`yIKuI+#W7g zOiBzo;&kxKyL!u}wG%j2Hfgj>xNg5p;<%);i)4l3vmCK^8Z!L*EZospB!TUMIGRGZdlaEMz z=3svDd!B@H>A9cZ&di?_G^u!5olfo7CjB2x`Lf~xZYU;?zx%dMdu1Qja7`*(*Z1-VM)x<#Ea8#q;z0i}c)I$ztaD0e F0sskoe3k$J literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/border-hover.png b/opt/ktouch/forest-light/border-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..a09fb3b7afe225fb5aae8f4ce39659bf7acdce95 GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|-zC7sn8b-nUl`yIKuIS|27e zF-m)OMPvwWV)(vaaATN9&RVp|9|))X1#sr4q+KihY?`%y+!7 z;pv=(TkBdF9qxbk&}^I^d RCILOi;OXk;vd$@?2>?Q*dh`GQ literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/border-invalid.png b/opt/ktouch/forest-light/border-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..2d01fa59db00aff34656baa1841ba9c94db34c78 GIT binary patch literal 444 zcmV;t0YmpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10ZB0$~`(@$a2pq32l%iUut~w6sXI2sPPUL^THzZ9-5>Q@cffrV5K3g&>DE zn{BIhv%v=S@DJD}#&unTNYM~_Ee-z0hxh&PyzlTlf@zxO+jEXWJjTnzEgy|nx{2tN zQGy`?kuCa1MiH9%M>88;#?b0rK|f=2xfwgU#9oNUFtj>?)Q5j{gQ4&|lE>E(qE}UF z=@i9_eGIKm;d~EUWI*3Bb)7(D6W_`jHiz7KR}L9@ViG};(C#w4mrIE4Kl6kaEIk## zGq(tU(a;dvMZ3pq8T@`%{NJ=3=c|tL92!Jv*gV4<9%xrY&?*rC7$oTO!LTd#)Q8?R0yU$=W mwC*6M3uSti;q`4Kub)panMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt=C7T^vIqTHoH>*wtzv(E2dh z>4}({tBy$0```9`3EC=7s}#5tQ+}5wc54=O|D5+A-On<1?N+<<&5tc61UYH4>UwG} z+}c*mH~&2Q-P-%5x6RIc=E>Hb66DKVe6GV#simPb_Tt;$aqWo*OniM0*L}YCc$Z$s z6rnX+*H*_JD|GSVTr}lU&9(g+O9Q+bGh{!>^DTMgphbuf3i5g~C%SyklR1h@TP{Q? zAG@*Zx#}^FWS*8{K1G&__xm4BvP`ddUpP;FTB_ss-h7}8ka7R}560cEl68`6rbhvN O&fw|l=d#Wzp$Pyq9gai* literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-accent.png b/opt/ktouch/forest-light/check-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..a789b5dc110684807ee5ab5cd4605160d24bbd0a GIT binary patch literal 526 zcmV+p0`dKcP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10h>ug zK~y-6tzIH;3|5?7&Mp;KBB1Wg5-5Qvf`L`^jEI<&4r_2D_>belGH5IZgj1IT2r zh$j=snP>Xc^-Z>95FnmRAQ^qL`%0xRkcwS)+8p$>bmYv-ioxpjT2_31WJ#v?!%%5p zo$h4C=SR2Lc=P#v&bgs0*Xd4HeS!bjUgYp$@QGH9EXhS-cY+%u^DMvph*EAgjNJ)- z+FGQm={ZZDX_RuaAq>FOIQB~19=cxiF+2E<{b-mf)s9_J3&J4JF9&~pLzZM_1}C`6 zUNf*T%6|0dK`bu_uo>FHQlW5?h!u?83j(bD*e)3Qmt-|7hL`eXtF$6@H@80;*w^|J zY24*OQ`C|%hec!3?ZqU75XWax0>Q8BhC`&%7lt6U(n5c0CzEb3Pwh4Q1!shc{Jsz? Q`~Uy|07*qoM6N<$f~2DBhX4Qo literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-basic.png b/opt/ktouch/forest-light/check-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..198019ef1f95cf6b72aa3a883d8f90b1762ee04c GIT binary patch literal 390 zcmV;10eSw3P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10TW3? zK~y-6t<}4Z!Y~kp;ooD&q9A^OL_(3czW>WaLZZSQg2Ey|VVNqstKG09B&VFwd^+8z zlv29i?|APCArNC^NVL}Ec}`Ij%w{vx?RHCbT@Qx3;O%ycc<)DJ05L|q_ecm~G}b1B zfK1V@O^gxw^f=2hoO6HXAA#0dHk%EL#bP{Yt!2GlV~pW&IE({-V*CA`>-G8`%(4un z)JIq+xZCYmtyYM9uli!0;PH55KA*E(E)kJI7=ZabIG@j?l&Gre3+jt?f&iDx1*H^K zRgqGnu4|g6d5gUS+omGIXP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10ia1l zK~y-6tsvN-m7wrw1o`16nqI<7)cAkKR|*A#EB7?ZB)XAB0_`U zTiYP4a5kdQLJ*W(2vMsRY2l)BAT<@O!1r-xdR)0sN1bQJ-|`&Jhr>BODWzQ6TA}~_ z8$Lzn*^3>Z5=%Fz4OG%mA0*)2#`kk`Sw4K#&i3J+5_C3>-(sR=5bda@uxD{SniU2R zOT^jO`HL36oBjI zdNFczWh^DwH#5TB(h6Qa?#9T`l`u&0<;jIPlK+zoHMSE8hAA)|T*FQaO9=u@efxkW zGzJ^nI7-BMH1dqEzkghbT?n3RYUcX+^=~b0>{1Y5?DKTm&`E@)8_Gi$#r9i>+Cb&C zf%nU*5gqkG9KV${mT%g0HiYm=DP?qPm4Vr}d|dj%Zfsu(%F|8i%4_Is2yx5tI0h#= VjH>K?5-9)x002ovPDHLkV1hW=@A&`# literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-tri-accent.png b/opt/ktouch/forest-light/check-tri-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..27bcb369ab4841d0eb192076a11ed5bf49442ad3 GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;^k7sn8b-sC_3fnfJ~IpdPt zxu*->oMUZUZ@=Go0@IG0mXd2ic_mNnnJhWQB~?P={hpdH_7z2phnK&P_b}p+{ZRPH zV9n{-Cl35^?q2`!*XC*w$zul&9Qg40ynfsC`S*+r{$CNx`0@8U|4}w00|SFSf4)p> z?7U)dn0?=$AIA>3m|kAcz&icN-g$E?Otq%&NEThbi`$)7hLg=mOKkVw<;xncd^a+; zR=>*g`00bRgiT?lOJ+5ey*kF@lX~wYPt6<~5oxs(M~?@*oqq1X50DqW$Jr;oxM$V> z@WtHEJn?lk$|n`%`1IJSONHCA_Ls16)(NsP#Cdo=h>5ma1oS?Gr>mdKI;Vst0F|_w AK>z>% literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-tri-basic.png b/opt/ktouch/forest-light/check-tri-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..a8970990f7632e0a00410db8650caf1b11589086 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|-3^7sn8b-nUaX@*Yv(ajiFS zGTFeuGr{h6zr%c1n?~+S5i!Lt56xY)V|VpZUHe+^X?uDMj}^)!q;+tFh?XlTEtwMF z<=7+W{l50i@4Dy`)wt`TeKX}xGdP4EJX4_bDWXhCsX*+dUx?_rGvAJ#m%6>JRwid{ z?6tGP|62De%(?gQ&AEMv`;7!=p6uYzTbiPF#`5sgpoEE+s{5m~#k$2O6uYezRb<)2 Y&?x6#7Tzopr0Fy*%CIA2c literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-tri-hover.png b/opt/ktouch/forest-light/check-tri-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..f38f8b95481d38d598550e21dc7d70ef5bc6cd5e GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;^k7sn8b-sC_3fuK5nof1zOF7gVbQG2fCEk5kID|G?U4$VuG!cXe7AKmTOSLvWaz8ff5Dd>URBSWJ$G_e z+sC2;V}nS|SHWi;qEnsOmIX~`VYB1Q>*5uD@F;afiGNxG*bBe&=NWx@6MLfI%{|vP wef#|rQv~>}#hd4L2|6#WHfm;>&%w+Pl;HVk>nR0Yp!XR(UHx3vIVCg!0G2S48vpP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10U$|4 zK~y-6?bJUn191Sx@uz3Fo>S7qB?b#g)5M=dqm7}7+6fkFm+%e@I=Gn^pa%9csHGEX zV$$#e?hGo?UjM{h`sccMOqQGca_fCxz$ef2L`c%IYd9@m;@NydZ#aNRs;nTJNa1LG zAG6vtU^YIO(eu1`yk^5`Q?!QDX0c*0eX>i-u5rYh8`n3MayVGqfhsFP>;4v|XXf!k z;^yTJSUBFI?RJEh`Lo>)7Yo~zdcy&mmtT}V`<;yxP|*=j7sn8b-nUl`yIKuI+#W7g zOiBzo;&kxKyL!u}wG%j2Hfgj>xNg5p;<%);i)4l3vmCK^8Z!L*EZospB!TUMIGRGZdlaEMz z=3svDd!B@H>A9cZ&di?_G^u!5olfo7CjB2x`Lf~xZYU;?zx%dMdu1Qja7`*(*Z1-VM)x<#Ea8#q;z0i}c)I$ztaD0e F0sskoe3k$J literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-unsel-hover.png b/opt/ktouch/forest-light/check-unsel-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..1690b975ad02dd3699c6763b795f767341119e5a GIT binary patch literal 398 zcmV;90df9`P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10UJp~ zK~y-6?bN+W!vFxp@oOAnOcW|D2!arF5}{fLYsG>nI5>!|RuJ?R9Ne61U%uhzvQt zhw)JvLSD_FW@wSYnxSERb_p>(aSVmIEY7x%F*T9G532&xjkP)rjQ{`u07*qoM6N<$g4TJb(EtDd literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/check-unsel-pressed.png b/opt/ktouch/forest-light/check-unsel-pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..2f4a5a8d9d0a688470b660987a082452abc440fa GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|+Pv7sn8b-sC_3fuK5nof1zOF7gVbQG20E4UtD;gKG38gBSTdQB?dHnQ2TEeEFE6Zm!ZadY+mmtT}V`<;yxP*IDgi(`mI@7t?}T+E6Btq<*I zWr=PH+Vj73Pt(!`TN9Rh3Nk4^&DwZ$Ur|>-JF{`W#N+PCf=&yj9I|ZR`pww7oAHcg z=GuutO3!%?6tn*E)nr}!HR|yHD^cH@_I}^{Kq6V6qdR)vLFqk88cXkR%N^e+{^j0# pW@iqGmmtT}V`<;yxP|-wB7sn8b-nUmbavnAiaJVS1 zIW@&1+%6<-=9>wYo1CYe^wHxD$}Lb=cjcH-;ugN-gZ}yo85xh|r~m$9s<bP0l+XkK D(1Kr9 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/combo-button-hover.png b/opt/ktouch/forest-light/combo-button-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..297c88b25bf49c087b96be92d786e5279971b805 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*J0&i(`mI@7t?}T+E6B4j1S9 zoZ5IHXzzc|J%_I~-QeD~X(f~5Q!Cf5zw8w}JZEgYG}Xn01w*DvC@Zos%nLVc*eaIH z)AxA6HLe5M?0?p6Z7a^pKXkw9{k8{lE{nxv-)!2j^QrWngY_(~N5n!xqYaj(us@sg n@!osc8|S$_G#~C|f53a@l8i>v``7bbP0l+XkKp3Yj{ literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/down-focus.png b/opt/ktouch/forest-light/down-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..70921a3c81ac9ce36d3ec09f2a569c11ab010890 GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^AT}!p8<4C?sm%aVoCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-XRE{-7_GfU5H8mo6*!F;I3nz=wIen r8x~fq5B8BTOmO@5UHxE@Wo4PVkZ-th{UXgepal$`u6{1-oD!ManMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&KtDWp2Mz_7`Z-;6%Y2(zHirILq1sXVZSQn_^J6xrHr15H zeosrfc5CUiUq;@VZ(WycyS7eEMO`WV!HnY)`=eg7t>|9Fq1YncV3@``XG+lRZMn&2 z*Us&1?YPsRZNSjJ?|u2b-~0~W>O(`X#%PwZduZnF`F{9BUFr=BC;e@^{sNuI;OXk; Jvd$@?2>@cuWEKDb literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/empty.png b/opt/ktouch/forest-light/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..202e3de5cc0f3b6284905c2e0cce05ccf2e265b0 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k&H|6fVjyh;!i=xBu@q4;BhGFVdQ&MBb@04Hc4rvLx| literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/hor-accent.png b/opt/ktouch/forest-light/hor-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..e92bc7d133db904ab6385e3ad4a3eeee7c69e5d6 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0xOo-U3d8WWTM{Qv*Io=uBM vElcXy0U%fr@y2XNtVNc>(}uze%bgjnN^m|7gTe~DWM4fgK04A literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/hor-basic.png b/opt/ktouch/forest-light/hor-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..eb18d1e4d205ab465662debda3f4778df196bf23 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0voo-U3d8WWTM{Qv*Io=uBM yElcXy0U#*)_9k+blt-y}pOlAXg3Onf0t~LMTqT)@B3*#G7(8A5T-G@yGywnwA2A~U literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/hor-hover.png b/opt/ktouch/forest-light/hor-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..92bc070d5566aa0fdd54587c844beb5473b921f3 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0xOo-U3d8WWTM{Qv*Io=uBM vElcXy0U%fr^xk+!tVNc>(}uze%bgjxlsJF9n6DfO)WYED>gTe~DWM4fgds63 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/notebook.png b/opt/ktouch/forest-light/notebook.png new file mode 100644 index 0000000000000000000000000000000000000000..5be429492fd93df907eb1b587abaf21019ecf43b GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&KtpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10*XmQ zK~zYI&6dwglW`o!U(eH=C6tjX=agx}Fxd)rbL-NgEpq5BLr>MT>|TNg3o6L!FHkq> zq>yw-oTLLaNVk)R+%z1Dn#>9Fpa-k{a2-VLnWEG7Sg+gj{(hd%_r>$`BP5f_Bq_ua zaRw$v@dUgC7K4OWBdkY%rk4E`Im(l$b5-bzbl41LYz8w~CltHtgjB&#U%$}mcCxUt zlu@CB(B>7;T-Cr$!_~b76B`@6b~|x?{&1{fsXVxLn>E$Gz608EGH=p7%X%jYHzB(70IS}FXfv46<`ry* z(Rd7pt7Tuw$pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10kBC# zK~zYI&6dHgqA(OdPn(ej+%yCtiR@(O|9^@*+(9BC!LEw1ptB(QgfWiK(RrQ~y}7wL zX>aKj&N;^n=N!fuj4@bi>HD5x80KmJgp?A`^9X_ftu#x~_Yv z(BDu>k)|nH>)V3PIm)u6s;d7gHfOb3k*4WoEzcjHAO}!Y6=hjo*1upp*14RNk|>G@ z!w}#10chKnrfI0_nj5YAG7g6W%jI(H%V8MYZnvBCoacFDS#}Yd)3z;np40dJ+a#4z zY`0saF=iqsrMxQ#;QKyVmfgH{ucGTZj4`9&HBl7Zl{@46K5-ns%MV}_wAN07VHiGK z;5)g`V68>Y`{rqfCbqPA>$n*TJq_vhj&)?dEN~!TB zan3no4Ez0lk|2a2j^iJB>Hs#I&6glRQ4}9BEC3`)!g{?1uBMqZO-YjE$E40&%fXF- l)fhuj6rVS+Uh_#K|8EO<@sy`G&7%MS002ovPDHLkV1hpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10+2~W zK~zYI&6Yh(6LA=apPr?l2^gtJ+eAJh!WqHDK++OO7`SQ|7?h!EqG+tfuo(%1i6#st zL`S0oD7p|F#E65~cF+MR1B<9_l@E~w4*4*tiJE>oln<{oP_RXx?SB9F-g|O)|GQTe zi9{mA5C{day9aT4UA&(eL-sB3)Bh{B?626V$v~H-<*@z$X0Z~pXd-QgdQF`wR`At_ z0ooj$OnsA+Dzp)K*?L;c=Q(z$GQMCa9Okz35f28QZL3%;7pqRtEHxsiQ~(Ie3;%;0 zKu3QMcbtz`j@1x-tg&+GY7N#33-x6+9L&iB;N#axdPm;SJ=}*s7)Y69d;e38?iVqO zCRF}lAaeZf6~#NJ%i2r(nVX2Y1#ugAv`F955evpRLG^PHS5X& zh`K`BPv1bJPD`2K zIK4x> zn;u2>&GRG4BmXW<2KwDOlpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10)I(F zK~zYI&6dwglW`o!U(eH=C6tjX=agx}Fxd)rbL-NgEpq5BLr>MT>|TNg3o6L!FHkq> zq>yw-oTLLaNVk)R+%z1Dn#>9Fpa-k{a2>?iGeynzn6KOS{e8pp;`{vi2+3qJNgA<4 zoPmi^JOM9(#USC;29v0%M|l!;t_ppT4x7P@&0r?$gknceNEiI{^$V?TCkrb} z85PLEY)orevh%Z9Gk(65RJ!@);IOC zK6C0^6`l1jxm0j**Zrx`EDl!-;nhegD^_V3eA0n9Ffl5tT(&~FryM}JrWBXs1xiI$ zDw~gl@c73NJpr$5asYKE`<`-Jlxs?5*0Ppfp~;LsR8_;E!ph$?o8xrRKj$!RE;|T55QF>)v*jKClk@ z$9UD(!F(iiq;e&yLYk`@u;3=InV4 kMLMi{6IQ(m#V#ZNZ}$iAezXM8mH+?%07*qoM6N<$f={7iG5`Po literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/on-basic.png b/opt/ktouch/forest-light/on-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..fd0f6c07bdd65f5b5ce6222e73f8bf9c9bf8c21c GIT binary patch literal 538 zcmV+#0_FXQP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10jEhs zK~zYI&6dH6qd*WqpUuWbb7@405O498|NoTS%uOL8m_D`9gS`hyMxB{#n2o!>lT{S> z=q{~tthJUI)>^dIXst2E&~+Vs-_Ohb3Lylp>*D)9N-305IF9qF&Y25duUCqqpl#c? z3jGNwB}tN?l=@oGT1!zBlx6u(#pdjGJCY=M#Nz(2204JTEGdfO5&weWT9=j+LJ)=_ zK@i}19so_#P}en8RWa_g%jLp$yB+$n?|XYXofhpm*L6wL^dUBfwrH&f!6ga9@TuG#&+~|)Xp+-fBaAUixd9La!H;W= zF^KWKFTpFP_g!t*bsxW(H83A)j^iL)*Io3bP1C$wd%N8x@n*9@_`d&crwMgk|G3uo zeZ+kBy_>44UOGi%3{_Q4a!RQ|P)fO!AJ$s3EPEpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10*OgP zK~zYI&6ZC{lW`n}pZ9G}3p8?>+aUjt=^JSf3Y#c+@Llbqhw@+rWzmZ87Kw+Tz(XK) zj5=sp7qLS|cF49J>Y&bpm&~@6e=U;H&q8 zv^%<(`Q}Th&}QUi8)&th=h&gj^##L`2)CV&crf^EOU0C2tU5u9+=M6y0EjCa+d&SX zbD)htuOibb4K?$pJK0ST~egLDCn}SZ+u%dn((RkX*4;)h9ExWJIwNw4DK;$F-9t1#xuu#UfL4=6W4V>OvYPm5 z_0E6d#qzIit(<_s{ZrSeT@eImcXZJ?(5tFsvRtli;F^36SToJE$W7R)PX0G3h~2qM z)eT&&J-eDI2Uds2MZ5hW(@S$(Dp#O0(rUSYESVDWKLi0n;Sdgw3#WIOceCU8{0sa@ u@yNeRn}K0>4yA@7WXXhFVnVad$p0I;_VACb?{zBx0000pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10xwBK zK~y-6rIk%j6Hyd~pP5ccGzugIu?BEq3X8TY+yH7yOd+9y8{`L=w$v>#q7o7}M1-(5 z4aFZ|bVD2>bb%6ER)%2Pg)|{5jWqIMz@|27%ea76(z&SAcX!Ww&XarZnK@U|G)<#K z*?f+n(KJ+UT^kVKgYErAVc~*B{<7rUv-3y`f7!sz#HU z2RA%)+1tgy{Qf>q)Bu6EFaHQCmB-P?kjsrMNdU+NqZ}B()SDMf%swlwWjHuyIk1BK z3(uL4h7W+*e2y_SP&TZf(HTG4d=6zU5<0d4=IUBTdVO4NZUG<^UuImLW^FT}%U$0| z5{!m1)LvfV4MlPLe;AYEpW|kL7WzxDDqhdskB;vx|jr97c zK7CdfuR3*x;U2HBCbGVY{O!k%&@3ENq-)KWh4JLx_V3T;B+}6RTmZ#wBu5Bjhp7b%hvPdeE zCY4E}t}No3yi*(&rE*$Zk+Qk0=A7{GdF!h%>0i}oG84QPKvoP2_g(J)f&mQo4q!AW z2VL&6x8rg2l@0TC^y9R)0!OCJkjstNaqGmefsTIecHb_pbpv)j8s_n@P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10dq-2 zK~y-6t(C#5>M#(7|J0g5K*_EJ=LLNKcd0v93D{Kzi=~Z*+=aAPYdmoDe5+4>FvdU#u`GpH3cdGm&S4zKm(7nTr9eb5#^i-5 zB{=8Mw(XCiB~?{{h~{-MJ$xbqfVOSnoO>2}?>~}FoK7b&^IbR{^;@j9NGYLY=C=)y zQi6!E-EIK@UDv@mhhZ3A_KxESX1)vG$@cp_%Ceke#u#ii8#GNbkJ2N|jN)>+WQ9cZ zB%4^RR!?1hr0;ta*XuQ#oR0aMTnOR+*!%@T2ozGvY_jXRw}t2PIU8TE*C@2s*`#w0 zF~*np?RJB;Hd~{$MzQ=BCx&4_(=_njBgTjrBfR%G91htI1NSK?jWJM4WoI#jfaCF) zEncFOf-we#5aR6^{01VrH)VR&s;WMg)pb2zWAoS$(MQs{uCd$g=Jm@3%gnIWet*F} c^35;cA4dGoTKq!(YybcN07*qoM6N<$f}--+asU7T literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/radio-hover.png b/opt/ktouch/forest-light/radio-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..0e9f2bd6a581edca37b62a46a6706e62641cf9ec GIT binary patch literal 679 zcmV;Y0$BZtP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10yIfP zK~y-6rIpW1lW`o!UwigEf{>1M*f#oc*rE;dP#1L?MD3Yf46M$fxJ5*D3H#-xQ(<-r zA|b8y2ZRTqn0qdf&2py=iND7t*0|6`Dzz%#zCMhlv^Yk!rQ z8)`dlhf@qpZ*J17`4|We{t=WfkF$*~wF60~13=0erN99CgM&QKp6A9AY%Q}CSXNKo zz90||Z2^<%6!$e>$*`>M`uj=UqkLIe5H^_&M!H+PG+U1V5R8n`tNDn;<_dbh ztuCU4L+EQxIPQE}niax^EJfYvLP;f{Y?7NaG9B!r67V zcrlb$6x0pnySU}<6lOlme@0qbUlE$wWr}pnc2rnjTwDHqZFVBT#|40-laTgQR13{u zWUMIg&CGjYy{4iHsn%i@n!TEjA3x&-_3w#w?)e9VnL5isB-P$1G$XM&TKlf>YGQ== zMuPZ8f|nB`wDw&kx)u{=T=sf&$y73P>d_@;i`>P3)xO;({Ffgi$$FXVYWsh|0B*N< zQ1tTFDZ3p`JkGX~VHcXu;j-5Q+g_V4wF6JnnH|HtO=r1w@_cSw@W2MbA$o?N{&i0e zRGGQy?##`sC=ei(P7w%)(5BuoK0lp1Ra2#r!xkH=y#d#udSt;b;1{;J&uDtbS(E?( N002ovPDHLkV1o7!IH>>t literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/radio-tri-accent.png b/opt/ktouch/forest-light/radio-tri-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5b54c35b0398180431356d0d46df5a1b1e0523 GIT binary patch literal 549 zcmV+=0^0qFP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10kTO% zK~y-6t(D788c`U4zd4L+n@CL|UKU0-GduveK_iez?Mi(B1Ly;ocW+oZz244blOTCozZQJ2g(U2v%x#vAhO{_~&z%Q-ohU$QLA!ba6J*ee{M74sC! zB`Q~E0KDmTqx20ERs)3909`KGUW#NFe);j8WG2m@^W&DucNysMu<|xe#5d|JG;eSD zm`RiVw)3FKPCPixQZ$B;BmhJs(T)rt^>v%|&zsFyKy91t$Qo?yZ;{n=HDT2>NM_P) z%Nl(6v_aK05POA#M-SjB99~q=^c+H~FSQeB#XO==E_F;;IQ)rxa&zIBT%Ahbef8Qg zcXWBe+HR_4ymEd1|JvAbctDRLBnfFeIcNPkEkC>1iJ{(pgxWXU5gCB5e*_U$1055F z6h9)O1{idEUo0Ew@i3wI5wc5WIU0Mh@cp}ax?FPYlq0@T;=$>*Wea1ogcLt;=eJpk z#z+Kbo-Lann`Lowt{Jy{uvtCF+HQ*9XMZ0Q4Z6LoP9>TvyH^NcmpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10TW3? zK~y-6t(DELgdhw?znT#eR^4f0-~Y)V(Usk3{8{uajAlm7%#C_hK;fhWC}d`a9cBhq zg{q?OdrZ>=0C3JBgaBp+GlPisOF?^K+qOt4VI0T90@?kEP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10kcU& zK~y-6t(8qr6G0e&pPepO3_yY`Q z+PzYivj}a#80Dh{EyQ~4F6*JTlHH=R?Q@%X=Y8@{X67BmaU2I1726`cyNQuE`B?Zw zv0MTm)ZWQRxF4yFBDEN80o9#~;ufy&WJzV-a&TVsEWS)nFw8SKg+3f_EVM5!STt67 zlU@6($W1an!MvPCC<*|g7VskjSkA8T%6L~F3#e|>kF3V)jrU}7W>r|REnXU{zGXFD ztS?isEkt^EldormriF*+Cm1;sVf-$&6&U#~#CBo7Wx@}~d#I;B&l(07^cf;IZZ?b^ z{rE;Iz2ccaDVP3S8#kWk^B)LBpva{s&gasRk9046UAGV;k^Yv*0PaKv5mFm%nJ}S6 z5xN$mHyCbO*4+_ed?<=g11eADRMW!e_mZ>))aohg!*P=F3E#5EcOR0_qQIqYGcTt} zj@`eu?9tc*=Ef)MV^4$4I;{X5v07*qoM6N<$f`St86aWAK literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/radio-unsel-accent.png b/opt/ktouch/forest-light/radio-unsel-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..fdfe1f0bf045123e624c5751f3caa1997e12053b GIT binary patch literal 676 zcmV;V0$crwP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10x?NM zK~y-6rPaSr8gUo~@Q2f`(B?2e{gG(Y<&L1NfW{R8OKPF?uXoenC0 zg+MAWQ3Xm(6X0MGu!B_5Dj-x7;o$86m;g;E@QnA(eRBC;-shDN5uq)WstAt|fG-h3 zB9lb+@*Du5l*z%3&A`BGVc@m6Gc?@(DkSaTPHYc0w*wQb3eV?scsQkoQYHrgoD_az z|7RTTh#yC}EZ)C(gJt!TR*@PJt5q^@Z_uJ`oxZNdfjU?gEA-6rnu5tl z&sa0zbc}T76)KArVmv|s6RZlF>A8+S_P=>FrN%g`f-fG#(8$x-k4xNRaHG?Gg8*hiuFTe~eHBuu2BawhnNLgfy7XyRY z%X28CEEs-l2K$G?z~J{o0qXHtFa}-=SHwRs=#GAcp3{I?=C#P=PZ3H*`o_ZP7=GnX zvBYWW<**amBb|ALa(@bakDfOktdCyvz~bB_quoULt71=>(Qcwo?d3p3RISy>>fE9Q z>l%ep(XLo98KHUWD%o6)mip=_SZ_QCt7{9R_Z0Plo#b)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10fk9K zK~y-6t<_D7t1uXb;ahDZh=u5OLM^oa{}?*Pz&Qti5P~F0kWwP0BnX1%r3BBQF^0OXiQ||o%Sh7{Ap`)S(ZI&?2kUwT4SxHD2iWy#kIb&EPFJ~3kYDX uMQe=^f;`W^E!f-bhI8)cg8iB;PrxVEG4245`Ha#40000pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10xwBK zK~y-6rPWVw(r_Hd@eg$#hi>R#7+K6Tt1EgEVP={|D9Q8*;LT)VdkE|q3_K0)fKNgq zg&R%^oHpoyI0suMjQtCg$YBG%9{6JzCOCK>e@$P9w%>0RL`1mORih3e9Yr`RLMr~m zv3w2y&?r^lrf0#k3*cE3Zr>Waek&B$!Qk$9xI%vDCN+4+y@o-@L8DXw0E+S%5(g3j z(l;EHOK|WXVSW0lSEP3JMuRqjUgAA#6wfDVP_bN+ES^)-;N+tOmQ{$6hIPEMF zhtkMk;y}VP?GrFOyMQmL$jD$oion7e!B|-new9mz=5~h0;`tPg$|o?-8C%OCxVuG5 zn=5qjyE63Xh09T!O!6ef2HaP)3AH@ZrJTDJ*k~Fz7f;-?>s~?KR$YqCPl&W)c^nh07*qo IM6N<$f^R7ylK=n! literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/radio-unsel-pressed.png b/opt/ktouch/forest-light/radio-unsel-pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..7428dc9863b085b7516acaabbc9f47f314df02aa GIT binary patch literal 512 zcmV+b0{{JqP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10gXvS zK~y-6t<}pe15q5u@$a0DC5aN+CRiY*!Mas;I<jdewdtTAR?!S zTs@_$25+A4(2_brd!MQm(Nb|lB7JTdcyxP)xPN)F47|%eejFPsA|!!mu5GXkY^t>* zx?Sy-fxWIygsSve2Kp5bBA|F_vpcKCTIwAPc6ku8O=eD=s2W@x3Q}W}3%eX}`w04m zE5>F9#_%g1;L~dpQYQ)akNg{(?jL1#aJ*E1d0?YSo#n_Tmw(*vHoKFBz*K2wKY{>; zX%J27XsI}f+jBD62LK#(jr6!WP!%u!E)ViIU%(q<9FH0t^lT0Q0000mmtT}V`<;yxP|+Pv7sn8b-sC_3fuK5nof1zOF7gVbQG20E4UtD;gKG38gBSTdQB?dHnQ2TEeEFE6Zm!ZadY+mmtT}V`<;yxP|+Pv7sn8b-sC_3fnfJ~IpdPt zxu*->oMUZUZ@=Go0@IG0mXd2ic_mNnnJhWQB~?P={hpdH_7z2phnK&P_b}p+{ZRPH zV9n{-Cl35^?q2`!*XC*w$zul&9Qg40ynfsC`S*+r{$CNx`0@8U|4}w00|SFSf4)p> z?7U)dn0?=$AIA>3m|k9xaOFTCvpcWKDTll=zg3dQP9HpbAZ2Zqhbi;hE4`94KsNud zsMIj?nb6(8@Rt7E#*ZMkzTaDS=)&Goxx*K&pGyMGa+~C6BNM~(?UfqO)w-8FLVuKa Z7=B)IW&8Zw@dePI44$rjF6*2UngG1Ol+XYG literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/rect-basic.png b/opt/ktouch/forest-light/rect-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..4fbd3c56bb0a2194d612b64a888d07f77d63ca6b GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*Jz1i(`mI@7t>zd51;g8w2HSsD$7M`@+{Xj?f6L-a|oNb23UhG}GYKG*ybCX?KrU)$+N$Sa2 zyR|U(dh!eLntAGuq4CKK@A_5=oq1zgmY&uk`TE_Rxdy(!w;f>B`=B4-vMf=_$wSkZ xx%ga55SMY-yWf6mr{*4GeKKd|)*=~p`C?}$?!@1x&jFpq;OXk;vd$@?2>^z1U%LPR literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/rect-hover.png b/opt/ktouch/forest-light/rect-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc43f64c567dc93f639e87626fb83551ead0f4f GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;jZ7sn8b-nUmb@*Y+YalM%R zZ@Ox^0b>&5d;50v2Hpi5r3Eq{skw?ApR$j6&Lh+OS9`xdw&*Yn@>;m{*i;vmY~6d+ z(^3;}eczjZ+imSvp)WI6ZCa6PZ!4!j+wRR4EY=aL|8A% z_cpa16ABb9=(^040gS#pja_wVHKHO25(xn-ltznY+)N i-`RKHm7{Np|1ri&`2RUpYWNmtEQ6=3pUXO@geCyG-anTB literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/right.png b/opt/ktouch/forest-light/right.png new file mode 100644 index 0000000000000000000000000000000000000000..cba43280811447b76b2831c001a1b16b9a54425c GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1g!3HF2ET&$Ca zJ@Pis@3?7}dQD0(@i=&}4~(88%$=W6b%zFMdkuPo?BDEggNa*L6SJtY>r(y0|a*c4(l; c*A2fIR%%;IXZ_E70dzEjr>mdKI;Vst00sDI761SM literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/scale-hor.png b/opt/ktouch/forest-light/scale-hor.png new file mode 100644 index 0000000000000000000000000000000000000000..86e2fcbe89915019b19404c84d1a987cc0df7e77 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP?4Uei(`mIZ}Ol2|NqysX)&o~ zNttCmP%xeF^v%u9A*KZ%sw-KfPFO@)^hkMFF)$=JaCLMC?rH?;WbkzLb6Mw<&;$Sw Ct1x2# literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/scale-vert.png b/opt/ktouch/forest-light/scale-vert.png new file mode 100644 index 0000000000000000000000000000000000000000..05312420b105ce17a2b690920e60648deffd0ec4 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP?5f;i(`mIZ}Ol2|Nqys$vk*- zb92Pe-rnBA%*2F4n$BtxrUh3H1Twqx%9s|ckllHuk|nMY zCBgY=CFO}lsSM@i<$9TU*~Q6;1*v-ZMd`EO*+>Bu@p`&AhH%VGwkUpfrk;VrfH`H^ S)2;kK2?kGBKbLh*2~7ahDj=}{ literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/sizegrip.png b/opt/ktouch/forest-light/sizegrip.png new file mode 100644 index 0000000000000000000000000000000000000000..8f0bddf3f9aca55e2d7f82e4ce939757fa26dd6e GIT binary patch literal 471 zcmV;|0Vw{7P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10c1%; zK~yM_jgn1^@-P&IpY$d*T~vaLE`(ymh5!Gz=*EQz4s_F60$udWw(nw?zQ{P>-JJtZ z&dE*GJkK*9PEi!(d5(yXWf|&IxbJ&Tr_-XXwFIB0j^p^EpU>y#a1=#sHX8txQf#-| z&ujlo+qQ@Z#+bj8z4v^5ec`=dysqmg%aX_A!8A?plfCy`uh&H%4hPojHRto0VHf~# z&Y|C>)^+_$hheyYKi=EgaUB2B)>@1)FW#RE^WIa}HE|r{oO`*mh>#@7H{AC<<2VvU z(E?A?MBBFCpCR~PuxT1ZgfI-zMNx3S-vQWcHthF%x~}7LxiAa^#u$#rBdgVl+wDeG zRVbxM)0803^X1g4s%YDmq9~T-48uTKmNZR6RaF4Y^Gu%S1R}D)N-0DHtzREOL=X{_ zQj4dxCdjf3Yb{!9(lo^wLy{yo=LmuT=Nw6r5QZUXnxeJFTFY*?;~#xqs!OlVh@t=h N002ovPDHLkV1jMZ*1P}! literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/spin-button-down-basic.png b/opt/ktouch/forest-light/spin-button-down-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..81535817395249ee1255a9c6524c47cff500f2df GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gy!3HEdYt9+~Db50q$YKTtzQZ8Qcszea3Q$n8 z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWD=vPZ!4!i{50L;%8^-*|eJF w?duE-0&fRMNSIb!c@Ws_&MOCGBz$6E2o7W`e*4E`El?AKr>mdKI;Vst024$l{r~^~ literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/spin-button-down-focus.png b/opt/ktouch/forest-light/spin-button-down-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..544545c96a17ae298a561c9aa79a8801c774990c GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gy!3HEdYt9+~Db50q$YKTtzQZ8Qcszea3Q$n8 z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWD<)PZ!4!i{9iAowDEO4;(nK z!D>@)W7LD={qj6MA;!GCJyJGVA66V(%+?QN7*sMaxHGa9PZT=u3)IWt>FVdQ&MBb@ E0MJ7*EdT%j literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/spin-button-up.png b/opt/ktouch/forest-light/spin-button-up.png new file mode 100644 index 0000000000000000000000000000000000000000..e04e75744321096d114f45a482baa618d69a31da GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*IMji(`mI@7psQd7Tw`91ea@ zZprw-RPjGkqJj0r#EgtJOa9twh?*a;-Fi)5+NEWV@luf>ubMI=hVt|t$L!v%*Xn-s zRlk4k)L{Ai=7qcG+Ya#NR~-J|u2f_ea-gV9*|jhj-_s-(0d5|;?5 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/tab-accent.png b/opt/ktouch/forest-light/tab-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..37b37c4fb950340fd8e726289dfce6738f8d4c82 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3oCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-;eE{-7{oymXx|Nmd##>n8q zocMCZ(Zyo@Q8rm0kr-bXb#Aas^}4e%>*U^fb0w0L^;fW;Nji3M-jNO4B^VU?xJ{ip S?kE8bWbkzLb6Mw<&;$T(gg>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt(Q|E{-7{oo}yir`TQa0>~-9x UE@G-Df#xxIy85}Sb4q9e0CanMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-;eE{-7{oymXx|Nmd##>n8q zocMCZ(Zyo@Q8rm0kr-bXb#AbPO4qE+I{CKktVEKs{tEUpNyjeEJF;QB1cSO9x2gM` Sdv!nq89ZJ6T-G@yGywou06%;H literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/thumb-hor-accent.png b/opt/ktouch/forest-light/thumb-hor-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..5179d9d4bd8666a6d9cf5eba1aa54809e953c73a GIT binary patch literal 316 zcmeAS@N?(olHy`uVBq!ia0vp^96&6>!3HEZNY`WoDb50q$YKTtzQZ8Qcszea3Q$n8 z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWIa0o-U3d8WWTM{0D;F>*b6~ za_62dcyo@G4Fs}7o!gAwr?9C8O0pfZ)|dawKQHFMhnK&_*-{Q1KY4hAUcM6ZXMWv% zZLiP2GhEO-zrN0J&z~>T8hK(70L@3lJ5 x-RS3rGJA{XH3GrGxVtS2Z|ToIDTWSIHIo-KiqMIPuW22WQ%mvv4FO#mgNeue-5 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/thumb-hor-basic.png b/opt/ktouch/forest-light/thumb-hor-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..ba727d75e4b977309fb7e1d97f668683adda2647 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^96&6>!3HEZNY`WoDb50q$YKTtzQZ8Qcszea3Q$n8 z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWIZyPZ!4!jfrn3ZRB-U=-Us;}~TFWVrYY?!3HEZNY`WoDb50q$YKTtzQZ8Qcszea3Q$n8 z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWIa0o-U3d8WWTM{0D;S{CNf? z?;{Ts{Q0NK1_IiywQWJuS=d|!Gue*qUN8Tje{S%h4=<00v!xt3e(>xDwWZS=e@>qm zaQLhEcgYI}&)e^pjH|0Ve&EmTh$Rod-?x|Gk^lmr(CGtTzCJHsGl>TX;_K>?6Ao!| zvo7XZv8-YCT;b&^`t0V+lcz4S^JCq1s*T5IsU9a!jm#a1NlY<&c&5HP$W3H@Uf;224&0q31oRYxr>mdKI;Vst0K>?D AhyVZp literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/thumb-vert-accent.png b/opt/ktouch/forest-light/thumb-vert-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..f793ad9fa33abb8aab1db753fce4774d7fe3c55e GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6cc@pN$v(U_S0=RXkaUN2`{ zk~{Zw!JBieZR_p#8&6=`ann+AO(?JAsXdb=r?{j_NW9-u^TocRi1G09_wgP^9I_t@ zKN+k!J^RFgKhE9jAO6~0Eh2gBz<~oFKA+cbn?C=Zk-`5fVi`aFe&;{RW@KPsu;|Z3eop$2PmiTN**+E&7~WVt+aTokQe%$~-3AN{o`>w-O@4jB1Lz+HPgg&ebxsLQ E0RE|luK)l5 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/thumb-vert-basic.png b/opt/ktouch/forest-light/thumb-vert-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..a58440d271a460cd9d5d740e9711fde093280797 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6d*sA`k0D z{v-*Zm5hN0zQ*(Nu3%!FkWrA7?w6OqU2$>Q-tTeQ2hTis*TJ!;v*W;-fY2@1COn%{ z-SbiE7rXuW=A5+S5)3XZXLxSj?cW&xuCK=T_q3TmrPP#8tm0C#I_9PMVz2Vu8*VNa ds@>O$%JVB*e|}lcG#Tgw22WQ%mvv4FO#qdRR<-~D literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/thumb-vert-hover.png b/opt/ktouch/forest-light/thumb-vert-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..faa6f6b69a3e9e85bb7955098f24e8e67c6a4cfa GIT binary patch literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6cc_H=O!(U_S0=RXis=g%`J zc^`S8;LktRw)OV=jVCbexM?Z5M&zrcSFDhPi0cwbiTC^J{@9oJaUNbSAMa_zA^V}Q zz<7=4%hZH_sVY|r{vNIE;z>?ONci#dx%uJK=if`rsF&_G`Sb60dlIvxgoH#~UES#e zE~1kcG)$a0vGJr=;QTDMTUyda22*#b7y!)*&Y8g=uJ63G+KBlv$VvC>KTlbh{@$rA z`MaJ`&Cj2`D$Nx)c1pYn>5P5%QCyyX=ggG1HHrG>FVdQ&MBb@0KO7{ AFaQ7m literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/tree-basic.png b/opt/ktouch/forest-light/tree-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..98c26e095f2c87c31010a69aa6eabb8b21c7a44b GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP?3_Si(`mIZ?aACvorNyxs`dJteF!0`GybN7O&r>+21GkCiCxvXmmtT}V`<;yxP?4#pi(`mIZ*quE+3)iXt_Kbs zIB;Nt)uvvpwpP9MNePR(JUtSw90+7~=an%nxZ=Q(u%t|sfgw7M?RK8vqaL6E44$rj JF6*2UngEtHGo}Cl literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/up.png b/opt/ktouch/forest-light/up.png new file mode 100644 index 0000000000000000000000000000000000000000..196e038a95d438968dcf5abbf40b7b7459016ac6 GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^AT}!p8<4C?sm%aVoCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt+o^T^vI=X2$j@@*Oe|ak%dv z%))hilj`mRYSX@|SP1Dg94lgF;qY@fpb${N*7U-_G-Fy^LradZt8Qe)E7kKhi4N0J zmDhe{UGRF>%;I%#XMaiRk!%;}JHVLHn0ncy`1u`n2SyQBL&kFl)~#B#`ujgo*Nf4b zTm3J83A_F^{FBZ6thN41o3>rM*RGu!IZNOWTf_<#Pd}wio37dYJY4<$u|*xjZTa|T W=Ib7wsSX1=m%-E3&t;ucLK6U?lxbc7 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/vert-accent.png b/opt/ktouch/forest-light/vert-accent.png new file mode 100644 index 0000000000000000000000000000000000000000..7dab8747c1ee4d1ed011f4cae5a0ba9cd13c0ec8 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^Ahrkx8<5=cZcP}F;wba4#Pn3(+M|NsB>Y(TJ7 wsGKe5LEL#i5Qj}|&8CP0D-H#=9*$OI@MvIHk=ZD}1*na|)78&qol`;+0NiaZ@c;k- literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/vert-basic.png b/opt/ktouch/forest-light/vert-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f61ec929dce1436a301c89dfb475313334a923 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^Ahrkx8<5=cZcP}F;wba4#Pn3(+M|NsB>Y;0_8 xo72x5B`_a5c8muEd?HdeHUzdV=4#*W#E@ISuJUUaH_##m22WQ%mvv4FO#lw1Ff9N8 literal 0 HcmV?d00001 diff --git a/opt/ktouch/forest-light/vert-hover.png b/opt/ktouch/forest-light/vert-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..bd8957d5fdbecb6ca463fa97a326e85f133c911e GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^Ahrkx8<5=cZcP}F;wba4#Pn3(+M|NsB>Y(Q|8 wEuJmsLD}In5Qj}|&8CP0D-H#=9*$OI@Ty=}F<-(e4b;Zq>FVdQ&MBb@0Ncqfb^rhX literal 0 HcmV?d00001 diff --git a/opt/ktouch/kscreen-debug b/opt/ktouch/kscreen-debug new file mode 100644 index 0000000..128ffdd --- /dev/null +++ b/opt/ktouch/kscreen-debug @@ -0,0 +1,170 @@ +#!/bin/bash + +get_touchdevice() +{ + # echo -n "" > $touchlist + for touch_id in $(xinput | grep -i -E 'touch|ILITEK|TSD' | cut -d '=' -f 2 | cut -f 1) + do + input_dev=$(xinput list-props $touch_id | grep "Device Node" | awk -F : '{print $2}' | awk -F \" '{print $2}' | awk '{print $1}') + touch_screen=$(udevadm info $input_dev | grep "ID_INPUT_TOUCHSCREEN") + + # dev=$(udevadm info $input_dev | grep "ID_SERIAL=" | cut -d "=" -f 2) + dev=$(xinput list-props $touch_id | grep "Device '" | cut -d "'" -f 2) + vid_t=$(udevadm info $input_dev | grep "ID_VENDOR_ID=" | cut -d "=" -f 2) + pid_t=$(udevadm info $input_dev | grep "ID_MODEL_ID=" | cut -d "=" -f 2) + path_t=$(udevadm info $input_dev | grep "ID_PATH=" | cut -d "=" -f 2) + # echo "$vid_t,$pid_t,$path_t,$dev,$touch_id" + echo "$dev" + echo " 输入设备id:$touch_id, 硬件id:$vid_t:$pid_t, 设备路径:$path_t" + done + +} + + +get_screeninfo() +{ + xrandr | grep connected | grep -v disconnected | while read -r screen ; do + # 提取显示器名称 + name=$(echo $screen | awk '{print $1}') + + # 检查是否是主屏幕 + if echo $screen | grep -q "primary"; then + primary="true" + # 主屏幕的信息在第4个字段 + resolution=$(echo $screen | awk '{print $4}') + else + primary="false" + # 非主屏幕的信息在第3个字段 + resolution=$(echo $screen | awk '{print $3}') + fi + + # 分离分辨率和坐标 + screen_resolution=$(echo $resolution | cut -d'+' -f1) + screen_x=$(echo $resolution | cut -d'+' -f2) + screen_y=$(echo $resolution | cut -d'+' -f3) + + # 输出结果 + echo "显示器名称: $name" + echo "分辨率: $screen_resolution" + echo "X坐标: $screen_x" + echo "Y坐标: $screen_y" + echo "主屏幕: $primary" + echo "---" + done +} + + +show_help(){ + echo ktouch debug。输入字母可以执行对应的功能 + echo " h help,帮助" + echo " q 退出debug程序" + echo " dev [id] 查看触摸设备详细信息,比如 t 10 查看id=10的触摸设备详细信息" + echo " map [屏幕] [id] 手动将对应id的设备校准到指定屏幕上,可能与系统设置冲突,需要重启才能恢复正常。" + echo " log 收集本工具的日志,生成到【主目录/家目录/个人文件夹】中" + echo " setting 开始进行一次触摸屏校准设置,并在终端打印出详细日志,但是不会保存。" + echo " automap 执行一次自动校准,并在终端打印出详细日志" + echo " service stop/start 停止或启动监控服务,包括其自启动也受影响" + echo " fix 修复程序,已经配置的信息会被删除" + echo " " +} + +map_to_output(){ + xinput map-to-output $2 $1 +} + +show_dev_info(){ + xinput list-props $1 +} + +service_control(){ + + if [ "$1" = "stop" ];then + sudo systemctl stop ktouch_daemon.service + sudo systemctl disable ktouch_daemon.service + elif [ "$1" = "start" ];then + sudo systemctl start ktouch_daemon.service + sudo systemctl enable ktouch_daemon.service + else + echo 参数不支持! + fi +} + + + +echo " ktouch 调试信息 " +echo ============================ +echo "系统触摸屏、签字笔及其输入节点包括:" +get_touchdevice + +echo +echo + +echo 系统显示器有: +get_screeninfo +echo +echo + + +echo 当前的触摸屏映射配置为: +cat /opt/ktouch/config +echo +echo + +show_help +while [[ 1 ]] +do + + echo -n "请输入命令: " + + read CMD + + if [ "$CMD" = "h" ];then + show_help + + elif [ "$CMD" = "q" ];then + echo -n 0 > /tmp/ktouch/setflag + exit 0 + + elif [ "$(echo $CMD | cut -d' ' -f1)" = "dev" ];then + show_dev_info $(echo $CMD | cut -d' ' -f2) + + elif [ "$(echo $CMD | cut -d' ' -f1)" = "map" ];then + #进行一次映射,则需要停掉触摸监控,在程序结束后再打开 + echo -n 1 > /tmp/ktouch/setflag + map_to_output $(echo $CMD | cut -d' ' -f2) $(echo $CMD | cut -d' ' -f3) + + elif [ "$CMD" = "log" ];then + kscreen-log + + elif [ "$CMD" = "setting" ];then + /opt/ktouch/screen_binder + + elif [ "$CMD" = "automap" ];then + kscreen-remap -s + + elif [ "$(echo $CMD | cut -d' ' -f1)" = "service" ];then + echo "请注意输入sudo密码" + service_control $(echo $CMD | cut -d' ' -f2) + echo 完成,执行结果为 $? + + elif [ "$CMD" = "fix" ];then + # 修复程序,流程为关闭程序监控,删除tmp,删除config,reconfigure一下,开启自启动 + echo "请注意输入sudo密码" + service_control stop + rm -rf /tmp/ktouch + rm -f /opt/ktouch/config + sudo dpkg-reconfigure ktouch >/dev/null 2>&1 + service_control start + echo 完成,执行结果为 $? + + + else + echo "命令有误,请输入 h 查看支持的命令。" + fi + echo + + + + +done + diff --git a/opt/ktouch/kscreen-fix-daemon b/opt/ktouch/kscreen-fix-daemon new file mode 100644 index 0000000..a3bc92b --- /dev/null +++ b/opt/ktouch/kscreen-fix-daemon @@ -0,0 +1,308 @@ +#!/bin/bash + + +# 脚本名称 +# SCRIPT_NAME="ktouch-fix-daemon" +SCRIPT_NAME=$0 +service_name="ktouch_daemon.service" +SERVICE_FILE="/etc/systemd/system/ktouch_daemon.service" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 打印函数 +print_info() { + echo -e "${GREEN}[INFO ]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN ]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_question() { + echo -e "${BLUE}[ ? ]${NC} $1" +} + +# 检查并获取root权限 +check_root() { + if [[ $EUID -ne 0 ]]; then + print_info "需要root权限来修改系统服务" + + + if sudo -n true 2>/dev/null; then + print_info "权限已自动获取,重启脚本" + # 已经有sudo权限,直接重新运行脚本 + exec sudo "$0" "$@" + else + # 需要输入密码 + print_info "请输入sudo密码" + exec sudo "$0" "$@" + fi + fi +} + +# 检查用户是否存在 +check_user_exists() { + local username="$1" + if id "$username" &>/dev/null; then + return 0 + else + return 1 + fi +} + +# 获取系统所有用户名列表 +get_all_users() { + # 获取所有普通用户和系统用户(UID >= 1000 或 UID < 1000但具有登录shell) + # getent passwd | cut -d: -f1 | sort + awk -F: '$3 >= 1000 && $7 ~ /\/(bash|sh|zsh|csh|ksh)$/ {print $1}' /etc/passwd +} + + + +# 停止服务 +stop_service() { + local service_name="ktouch_daemon.service" + + print_info "停止 $service_name 服务..." + + if systemctl is-active --quiet "$service_name"; then + systemctl stop "$service_name" + if [[ $? -eq 0 ]]; then + print_info "服务已停止" + else + print_error "停止服务失败" + return 1 + fi + else + print_info "服务未运行" + fi + return 0 +} + +# 禁用服务自启动 +disable_service() { + local service_name="ktouch_daemon.service" + + print_info "禁用服务自启动..." + + if systemctl is-enabled --quiet "$service_name" 2>/dev/null; then + systemctl disable "$service_name" + if [[ $? -eq 0 ]]; then + print_info "服务自启动已禁用" + else + print_error "禁用服务自启动失败" + return 1 + fi + else + print_info "服务自启动未启用" + fi + return 0 +} + +# 修改服务文件中的用户名 +modify_service_user() { + local service_file="$1" + local new_user="$2" + local backup_file="${service_file}.backup.$(date +%Y%m%d_%H%M%S)" + + print_info "备份服务文件: $backup_file" + cp "$service_file" "$backup_file" + + print_info "修改服务文件中的用户名..." + + # 检查服务文件是否包含User=行 + if grep -q "^User=" "$service_file"; then + # 替换现有的User=行 + sed -i "s/^User=.*/User=$new_user/" "$service_file" + else + # 在[Service]部分添加User=行 + if grep -q "\[Service\]" "$service_file"; then + sed -i "/\[Service\]/a User=$new_user" "$service_file" + else + # 如果没有[Service]部分,在文件末尾添加 + echo -e "\n[Service]\nUser=$new_user" >> "$service_file" + fi + fi + + if [[ $? -eq 0 ]]; then + print_info "服务文件修改成功" + return 0 + else + print_error "服务文件修改失败" + # 恢复备份 + cp "$backup_file" "$service_file" + return 1 + fi +} + +# 重新加载systemd配置 +reload_systemd() { + print_info "重新加载systemd配置..." + systemctl daemon-reload + if [[ $? -eq 0 ]]; then + print_info "systemd配置已重新加载" + return 0 + else + print_error "重新加载systemd配置失败" + return 1 + fi +} + +# 启用服务自启动 +enable_service() { + local service_name="ktouch_daemon.service" + + print_info "启用服务自启动..." + + systemctl enable "$service_name" + if [[ $? -eq 0 ]]; then + print_info "服务自启动已启用" + return 0 + else + print_error "启用服务自启动失败" + return 1 + fi +} + +# 启动服务 +start_service() { + local service_name="ktouch_daemon.service" + + print_info "启动服务..." + + systemctl start "$service_name" + if [[ $? -eq 0 ]]; then + print_info "服务已启动" + return 0 + else + print_error "启动服务失败" + print_info "可以使用 'systemctl status $service_name' 查看详细错误信息" + return 1 + fi +} + +# 显示服务状态 +show_service_status() { + local service_name="ktouch_daemon.service" + + print_info "当前服务状态:" + systemctl status "$service_name" --no-pager -l +} + +# 主函数 +main() { + print_info "=== ktouch_daemon 服务用户修改工具 ===" + + if [ ! -f $SERVICE_FILE ];then + print_error "服务文件不存在,请重新安装ktouch软件!" + exit 1 + + fi + + # 检查root权限 + check_root "$@" + + # 显示当前服务配置 + print_info "当前服务配置:" + grep -E "User=" "$SERVICE_FILE" 2>/dev/null || print_warning "无法获取当前配置" + + # 获取用户名 + while true; do + echo + print_question "正在修改 ktouch_daemon 监控服务的执行用户,请在下面输入需要执行的用户名" + echo -n "用户名: " + read -r username + + # 检查输入是否为空 + if [[ -z "$username" ]]; then + print_error "用户名不能为空,请重新输入" + continue + fi + + # 检查用户是否存在 + if check_user_exists "$username"; then + # print_info "用户 '$username' 存在" + break + else + print_error "用户 '$username' 不存在,请重新输入" + print_info "可用的用户列表:" + get_all_users | head -10 | tr '\n' ' ' + echo -e "\n(显示前10个用户,完整列表请查看 /etc/passwd)" + fi + done + + # 确认操作 + echo + print_info "即将执行以下操作:" + print_info "1. 停止 ktouch_daemon.service 服务" + print_info "2. 禁用服务自启动" + print_info "3. 修改服务文件中的用户为: $username" + print_info "4. 重新启用服务自启动" + print_info "5. 启动服务" + echo + echo -n " 是否继续? (y/N): " + read -r confirm + + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + print_info "操作已取消" + exit 0 + fi + + # 执行操作 + echo + print_info "开始修改服务配置..." + + # 停止服务 + if ! stop_service; then + exit 1 + fi + + # 禁用服务自启动 + if ! disable_service; then + exit 1 + fi + + # 修改服务文件 + if ! modify_service_user "$SERVICE_FILE" "$username"; then + print_error "修改失败!" + exit 1 + fi + + # 重新加载systemd配置 + if ! reload_systemd; then + exit 1 + fi + + # 启用服务自启动 + if ! enable_service; then + exit 1 + fi + + # 启动服务 + if ! start_service; then + exit 1 + fi + + # 显示最终状态 + echo + print_info "=== 操作完成 ===" + show_service_status + + echo + print_info "服务用户已成功修改为: $username" +} + +# 错误处理 +set -euo pipefail + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/opt/ktouch/kscreen-log b/opt/ktouch/kscreen-log new file mode 100644 index 0000000..4b6052d --- /dev/null +++ b/opt/ktouch/kscreen-log @@ -0,0 +1,64 @@ +#!/bin/bash +# 搜集日志和配置文件,用于debug分析 + +log_path="/opt/ktouch" +now=$(date "+%m%d-%H%M%S") + + +tdir="/tmp/klog_$now" + +mkdir $tdir && cd $tdir + +echo "正在收集日志文件,请注意需要输入sudo密码。" + +# cp /opt/ktouch/*log* $tdir +# cp /opt/ktouch/config $tdir +# cp /opt/ktouch/*.txt $tdir + + +# 采集系统信息 +export DISPLAY=:0 +xinput > $tdir/xinput_info.txt +for id in $(xinput list --id-only); do + xinput --list-props "$id" >> $tdir/xinput_props.txt + echo -e "\n\n" $tdir/xinput_props.txt +done +xrandr > $tdir/xrandr_info.txt +sudo dmidecode > $tdir/dmidecode.txt +lsusb >> $tdir/lsusb.txt +lsusb -t >> $tdir/lsusb.txt +lsblk -f >> $tdir/lsblk.txt +lspci >> $tdir/lspci.txt + +if [ -f /etc/kylin-release ]; then + cat /etc/os-release /etc/kylin-release 2>/dev/null >> $tdir/os-version.txt +elif [ -f /etc/uos-release ]; then + cat /etc/os-release /etc/uos-release /etc/os-version 2>/dev/null >> $tdir/os-version.txt +else + cat /etc/os-release 2>/dev/null >> $tdir/os-version.txt +fi + +# 采集系统的日志 +sudo cp /var/log/messages* $tdir >/dev/null 2>&1 +sudo cp /var/log/boot* $tdir >/dev/null 2>&1 +sudo cp /var/log/kern* $tdir >/dev/null 2>&1 +sudo cp /var/log/Xorg* $tdir >/dev/null 2>&1 +sudo cp /var/log/syslog* $tdir >/dev/null 2>&1 + + +# 将程序和日志整体打包 +sudo cp /etc/systemd/system/ktouch_daemon.service $tdir/ktouch_daemon.service +sudo chmod 777 -R $tdir +cp -r /opt/ktouch $tdir/ktouch + + +ls -la ./ >> $tdir/fileinfo.txt +ls -la ./ktouch >> $tdir/fileinfo.txt + +# 打包 +echo "正在打包文件,将会生成在主目录/家目录/个人文件夹" + +cd /tmp +tar -czf ~/ktouch_log-$now.tar.gz ./klog_$now + +rm -rf /tmp/klog \ No newline at end of file diff --git a/opt/ktouch/kscreen-remap b/opt/ktouch/kscreen-remap new file mode 100644 index 0000000..d411299 --- /dev/null +++ b/opt/ktouch/kscreen-remap @@ -0,0 +1,52 @@ +#!/bin/bash +export DISPLAY=:0 + +LOG_FILE=/opt/ktouch/touchscreen.log + +# 日志函数 +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') kscreen-remap : $1" | tee -a "$LOG_FILE" +} + +log_message "用户($USER)手动执行映射" + +# 清空UOS的触摸屏设置信息 +if [ -f /usr/bin/gsettings ];then + gsettings set com.deepin.dde.display map-output '' > /dev/null 2>&1 + if [ "$?" == "0" ];then + echo "成功清除UOS系统设置中的触摸屏设置信息" + fi +fi + +# 更新一次显示器信息 +if [ ! -d /tmp/ktouch ];then + mkdir -p /tmp/ktouch +fi +touch /tmp/ktouch/screen.txt +/opt/ktouch/screen_ds_once /tmp/ktouch/screen.txt + + +if [ ! -f "/opt/ktouch/config" ];then + # 配置文件不存在 + log_message "配置文件不存在,不执行" + notify-send -u critical --icon=dialog-warning -t 9000 "触摸屏映射工具 ktouch" "配置文件不存在,请先进行配置!" + exit 1 +fi + +if [ "$1" == "" ];then + /opt/ktouch/touch_set + notify-send -u low --icon=view-fullscreen -t 3000 "触摸屏映射工具 ktouch" "映射操作完成,请点击屏幕检查。" + exit 0 +fi + +if [ "$1" == "-s" ];then + # 单次映射,用于开机自启动脚本保障使用。 + log_message "$1 参数 = 单次执行" + /opt/ktouch/touch_set + exit 0 +fi + +echo "请不要携带任何参数执行。本次未作任何操作。" +log_message "映射命令有参数传入($1), 忽略" + +exit 127 diff --git a/opt/ktouch/kscreen-remap-daemon b/opt/ktouch/kscreen-remap-daemon new file mode 100644 index 0000000..158d7c2 --- /dev/null +++ b/opt/ktouch/kscreen-remap-daemon @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import time +import logging +import signal +import hashlib +import subprocess +from pathlib import Path +from threading import Thread +from time import sleep + +# 配置X11环境变量 +os.environ['DISPLAY'] = ':0' +# os.environ['XAUTHORITY'] = '/home/pi/.Xauthority' # 根据实际情况调整路径 + +# 全局变量 +processes = {} # 存储进程对象 +monitor_thread = None # 监控线程 +running = True # 控制循环运行 +isSettings = False +SettingFlagPath="/tmp/ktouch/setflag" +apppath="/opt/ktouch" +display_monitor_process = None # 存储display_monitor进程对象 + + +# 配置日志 +def setup_logging(): + log_file = os.path.join(apppath, 'touchscreen.log') + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s daemon.py [%(levelname)s] %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] + ) + return logging.getLogger(__name__) + +logger = setup_logging() + +# 信号处理函数 +def signal_handler(signum, frame): + global running + logger.info("Received signal %d, shutting down...", signum) + running = False + cleanup() + +def cleanup(): + """清理函数,停止所有子进程""" + logger.info("清理子进程") + for proc_name, proc_info in processes.items(): + try: + proc = proc_info['process'] + if proc and proc.poll() is None: # 进程还在运行 + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + logger.info("关闭了进程: %s", proc_name) + except Exception as e: + logger.error("关闭进程失败 %s: %s", proc_name, e) + processes.clear() + + +# 检查display_monitor.py是否在运行 +def check_display_monitor_running(): + """检查display_monitor.py是否已经在运行""" + try: + # 使用pgrep检查进程 + result = subprocess.run(['pgrep', '-f', 'display_monitor.py'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return result.returncode == 0 and len(result.stdout.strip()) > 0 + except Exception as e: + logger.error("检查display_monitor.py进程状态失败: %s", e) + return False + +# 启动display_monitor.py +def start_display_monitor(): + """启动display_monitor.py脚本""" + global display_monitor_process + + # 检查脚本是否存在 + display_monitor_script = "/opt/ktouch/display_monitor.py" + if not os.path.exists(display_monitor_script): + logger.warning("display_monitor.py脚本不存在: %s", display_monitor_script) + return False + + # 检查是否已经在运行 + if check_display_monitor_running(): + logger.info("display_monitor.py已经在运行") + return True + + try: + # 设置X11环境 + env = os.environ.copy() + env['DISPLAY'] = ':0' + + # 启动display_monitor.py,使用nohup使其在后台独立运行 + display_monitor_process = subprocess.Popen( + ['nohup', 'python3', display_monitor_script, '&'], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + preexec_fn=os.setpgrp # 创建新的进程组,使子进程独立 + ) + + # 等待一下让进程启动 + time.sleep(2) + + # 检查是否成功启动 + if check_display_monitor_running(): + logger.info("成功启动display_monitor.py (PID: %d)", display_monitor_process.pid) + return True + else: + logger.error("启动display_monitor.py失败") + return False + + except Exception as e: + logger.error("启动display_monitor.py时发生错误: %s", e) + return False + +# 1. 日志文件轮转函数 +def rotate_logs(): + log_files = [os.path.join(apppath,'sub_modules.log'), os.path.join(apppath,'touchscreen.log')] + max_size = 10 * 1024 * 1024 # 10MB + max_backups = 7 + + for log_file in log_files: + if os.path.exists(log_file) and os.path.getsize(log_file) > max_size: + #print(f'文件{log_file}的大小是{os.path.getsize(log_file)}') + # 删除最旧的备份文件 + oldest_backup = "%s.%d" % (log_file, max_backups) + if os.path.exists(oldest_backup): + os.remove(oldest_backup) + logger.info("清理过早的日志: %s", oldest_backup) + + # 重命名现有备份文件 + for i in range(max_backups-1, 0, -1): + old_name = "%s.%d" % (log_file, i) + new_name = "%s.%d" % (log_file, i+1) + if os.path.exists(old_name): + os.rename(old_name, new_name) + # logger.info("Renamed %s to %s", old_name, new_name) + + # 重命名当前日志文件 + os.rename(log_file, "%s.1" % log_file) + # logger.info("Rotated log file: %s -> %s.1", log_file, log_file) + +# 2. 检查并创建所需文件 +def initialize_files(): + ktouch_dir = Path("/tmp/ktouch") + files_to_create = [ + "screen.txt", + "touch.txt", + "touch_need_update.txt", + "usbadd.txt", + "setflag" + ] + + # 创建目录 + if not ktouch_dir.exists(): + ktouch_dir.mkdir() + logger.info("创建状态缓存目录 /tmp/ktouch ") + + # 创建或初始化文件 + for file_name in files_to_create: + file_path = ktouch_dir / file_name + with open(str(file_path), 'w') as f: + f.write('1') + logger.info("初始化缓存文件 %s: to 1", file_path) + + +# 启动单个后台程序 +def start_process(proc_name, proc_args): + """启动单个进程并添加到监控列表""" + # 设置X11环境 + env = os.environ.copy() + env['DISPLAY'] = ':0' + + try: + proc = subprocess.Popen(proc_args, env=env) + processes[proc_name] = { + 'process': proc, + 'args': proc_args, + 'start_time': time.time(), + 'restart_count': 0 + } + logger.info("拉起子进程 %s (PID: %d)", proc_name, proc.pid) + return True + except Exception as e: + logger.error("进程启动失败 %s: %s", proc_name, e) + return False + + +# 3. 启动后台程序(添加X11环境支持) +def start_background_processes(): + process_configs = { + 'screen_ds': ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"], + 'touch_ds': ["/opt/ktouch/touch_ds", "/tmp/ktouch/touch.txt", "/tmp/ktouch/touch_need_update.txt"], + 'usb_ds': ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"] + } + + for proc_name, proc_args in process_configs.items(): + start_process(proc_name, proc_args) + + +# 停止所有子进程 +def stop_all_processes(): + """停止所有监控的子进程""" + logger.info("停止所有子进程") + for proc_name, proc_info in list(processes.items()): + try: + proc = proc_info['process'] + if proc and proc.poll() is None: # 进程还在运行 + proc.terminate() + try: + proc.wait(timeout=3) + logger.info("成功停止进程: %s", proc_name) + except subprocess.TimeoutExpired: + proc.kill() + logger.warning("强制停止进程: %s", proc_name) + # 从监控列表中移除 + del processes[proc_name] + except Exception as e: + logger.error("停止进程失败 %s: %s", proc_name, e) + + +# 检查暂停标志文件 +def check_pause_flag(): + """检查setflag文件,返回是否需要暂停""" + + if not os.path.exists(SettingFlagPath): + return False + + try: + with open(SettingFlagPath, 'r') as f: + content = f.read().strip() + return content == '1' + except Exception as e: + # logger.error("读取setflag文件失败: %s", e) + return False + + +# 进入设置轮空的状态 +def handle_pause_state(): + """处理暂停和恢复逻辑""" + global isSettings + + if should_pause and not paused: + # 进入暂停状态 + logger.info("检测到setflag=1,进入暂停状态") + paused = True + stop_all_processes() + logger.info("已暂停所有监控和子进程") + + elif not should_pause and paused: + # 恢复运行状态 + logger.info("检测到setflag=0或文件不存在,恢复运行状态") + paused = False + # 重新启动后台进程 + start_background_processes() + logger.info("已恢复所有监控和子进程") + + return paused + + + +# 监控和重启进程 +def monitor_processes(): + """监控进程状态,如果进程退出则重启""" + global running, isSettings + + buff_isSettings = isSettings + + while running: + try: + if not isSettings == buff_isSettings: + # 值变化,开始操作 + if isSettings: + stop_all_processes() + else: + start_background_processes() + + buff_isSettings = isSettings + + if isSettings: + time.sleep(5) + continue + + + for proc_name, proc_info in list(processes.items()): + proc = proc_info['process'] + returncode = proc.poll() + + if returncode is not None: # 进程已退出 + logger.warning("进程 %s (PID: %d) 已退出,代码 %d", proc_name, proc.pid, returncode) + + # 限制重启次数,避免无限重启 + proc_info['restart_count'] += 1 + if proc_info['restart_count'] > 128: # 最多重启128次 + logger.error("启动 %s 进程被结束超过128次,决定不再重启。请检查日志文件。", proc_name) + del processes[proc_name] + continue + + # 等待一段时间再重启 + time.sleep(2) + + # 重新启动进程 + if start_process(proc_name, proc_info['args']): + logger.info("重启进程: %s", proc_name) + else: + logger.error("重启进程失败: %s", proc_name) + + # 检查是否有进程需要启动(初始启动失败的情况) + expected_processes = ['screen_ds', 'touch_ds', 'usb_ds'] + for proc_name in expected_processes: + if proc_name not in processes: + logger.warning("发现 %s 消失, 准备重新启动", proc_name) + # 这里需要根据进程名重新构建参数 + if proc_name == 'screen_ds': + start_process(proc_name, ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"]) + elif proc_name == 'touch_ds': + start_process(proc_name, ["/opt/ktouch/touch_ds", "/tmp/ktouch/touchmap.txt", "/tmp/ktouch/touch_need_update.txt"]) + elif proc_name == 'usb_ds': + start_process(proc_name, ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"]) + + time.sleep(5) # 每2秒检查一次进程状态 + + except Exception as e: + logger.error("进程监控出现问题: %s", e) + time.sleep(5) + + +# 4. 执行touch_remap并重置文件(添加X11环境支持) +def run_touch_remap(): + # 设置X11环境 + env = os.environ.copy() + env['DISPLAY'] = ':0' + + try: + # 执行touch_remap程序 + result = subprocess.run(["/opt/ktouch/touch_set", "/tmp/ktouch/touch.txt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=env) + if result.returncode == 0: + logger.info("执行映射(touch_set)成功") + else: + logger.error("执行映射(touch_set)失败,错误信息为 %d: %s", result.returncode, result.stderr) + except Exception as e: + logger.error("无法执行touch_set映射程序!!!!!: %s", e) + + # 重置文件为0 + ktouch_dir = Path("/tmp/ktouch") + files_to_reset = [ + "touch_need_update.txt", + "usbadd.txt", + "setflag" + ] + + for file_name in files_to_reset: + file_path = ktouch_dir / file_name + try: + with open(str(file_path), 'w') as f: + f.write('0') + # logger.info("Reset %s to 0", file_path) + except Exception as e: + # logger.error("Failed to reset %s: %s", file_path, e) + pass + +# 5. 监控文件变化 +def monitor_files(): + global isSettings + # 记录screen.txt的初始状态 + screen_file = "/tmp/ktouch/screen.txt" + last_screen_content = "" + + # 启动时读取以一下屏幕文件 + if os.path.exists(screen_file): + with open(screen_file, 'r') as f: + last_screen_content = f.read().strip() + + logger.info("开始主程序监控") + buff_mainloop_isSettings = check_pause_flag() + + while running: + # 检查是否在设置过程中,是的话则暂停循环,并关闭三个子进程 + isSettings = check_pause_flag() + if not buff_mainloop_isSettings == isSettings: + logger.info("设置标志变化:%d", isSettings) + # 变化,如果设置结束则触发一起映射 + if not isSettings: + run_touch_remap() + + buff_mainloop_isSettings = isSettings + + if isSettings: + sleep(2) + continue + + + # 检查是否需要执行touch_remap + need_update = False + + # 检查usbadd.txt和touch_need_update.txt + for file_name in ["/tmp/ktouch/usbadd.txt", "/tmp/ktouch/touch_need_update.txt"]: + try: + with open(file_name, 'r') as f: + content = f.read().strip() + if content == '1': + logger.info("文件 %s 内容变为1,触发重映射", file_name) + need_update = True + except Exception as e: + logger.error("读取文件失败 %s: %s", file_name, e) + + # 检查screen.txt是否发生变化 + try: + with open(screen_file, 'r') as f: + current_content = f.read().strip() + if current_content != last_screen_content: + logger.info("文件 screen.txt 内容变化,触发重映射") + need_update = True + last_screen_content = current_content + except Exception as e: + logger.error("读取文件失败 %s: %s", screen_file, e) + + # 如果需要更新,执行touch_remap + if need_update: + run_touch_remap() + + # 等待一段时间再次检查 + time.sleep(1) + +# 检查X11环境是否可用 +def check_x11_environment(): + try: + # 尝试运行一个简单的X11命令来检查环境 + result = subprocess.run(['xset', '-q'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=5) + if result.returncode == 0: + logger.info("X11 environment is available") + return True + else: + logger.warning("X11 environment check failed") + return False + except Exception as e: + logger.error("Error checking X11 environment: %s", e) + return False + +# 主函数 +def main(): + global monitor_thread, running + has_config_file=True + try: + # 注册信号处理 + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + logger.info("==============kscreen_remap_daemon 程序启动====================") + + # 检查X11环境 + x11_try=3 + while x11_try: + if not check_x11_environment(): + logger.warning("主程序:X11 environment may not be available, 程序将继续运行但无效果") + x11_try -= 1 + sleep(1) + else: + x11_try = 0 + + # 1. 日志轮转 + rotate_logs() + logger.info("主程序:处理日志 OK") + + # 2. 初始化文件 + initialize_files() + logger.info("主程序:初始化和检查文件 OK") + + + # 2.5 启动屏幕设置的后台监控进程 + # 检查并启动display_monitor.py(UOS中) + if os.path.exists('/etc/os-version'): + logger.info("检查并启动display_monitor.py...") + if start_display_monitor(): + logger.info("display_monitor.py启动成功或已在运行") + else: + logger.warning("display_monitor.py启动失败,但主程序将继续运行") + + # ========如果没有配置文件,则不用运行监控=========== + if not os.path.exists("/opt/ktouch/config"): + logger.info("配置文件不存在, 进入等待模式") + has_config_file = False + while True: + if not running: + break + time.sleep(5) + if os.path.exists("/opt/ktouch/config"): + logger.info("配置文件出现, 进行一次映射,然后开始监控") + run_touch_remap() + break + + + + # 3. 启动后台进程 + start_background_processes() + logger.info("主程序:启动子进程 OK") + + # 启动进程监控线程 + monitor_thread = Thread(target=monitor_processes) + monitor_thread.daemon = True + monitor_thread.start() + logger.info("主程序:启动进程监控 OK") + + sleep(5) + # 4. 首次执行touch_remap + while True: + if os.path.exists("/tmp/ktouch/screen.txt"): + break + # 如果screen.txt不存在可能引起故障 + + run_touch_remap() + logger.info("主程序:执行一次映射 OK") + + sleep(2) + # 5. 开始监控文件变化 + logger.info("主程序:开始监控loop") + monitor_files() + + + except Exception as e: + logger.error("Unexpected error in main: %s", e) + running = False + cleanup() + sys.exit(1) + finally: + cleanup() + logger.info("Daemon script stopped") + +if __name__ == "__main__": + main() diff --git a/opt/ktouch/kscreen-setup b/opt/ktouch/kscreen-setup new file mode 100644 index 0000000..84a4be7 --- /dev/null +++ b/opt/ktouch/kscreen-setup @@ -0,0 +1,141 @@ +#!/bin/bash + +workpath=/opt/ktouch +tmppath=/tmp/ktouch +INPUT_FILE="$workpath/touch_dis_table.txt" +OUTPUT_FILE="$workpath/config" +LOG_FILE="$workpath/touchscreen.log" + +# 日志函数 +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') kscreen-setup : $1" | tee -a "$LOG_FILE" +} + +mkdir -p /tmp/ktouch +cd $workpath + +# 首先往setflag写入1停止监控 +echo -n "1" > $tmppath/setflag + +# 消灭监控进程 +log_message "关闭监控进程" +sleep 2 + +# 环境变量 +export DISPLAY=:0 + +# 备份config文件 +echo -n "" > $workpath/config.bk +cp $workpath/config $workpath/config.bk + +# 启动检测 +log_message "启动GUI化配置程序" +$workpath/screen_binder +log_message "配置过程结束" + + +# ==============解析文件=============== + +# 检查输入文件是否存在 +if [[ ! -f "$INPUT_FILE" ]]; then + log_message "错误: 输入文件 $INPUT_FILE 不存在" + exit 1 +fi + +echo -n "" > $workpath/config + +log_message "开始处理触摸屏设备信息" +# 逐行读取输入文件 +while IFS='|' read -r display_name touchscreen_name touchscreen_id event_path; do + # 去除可能的空白字符 + display_name=$(echo "$display_name" | xargs) + touchscreen_name=$(echo "$touchscreen_name" | xargs) + event_path=$(echo "$event_path" | xargs) + + log_message "处理设备: $touchscreen_name, 事件路径: $event_path" + + # 检查事件路径是否为空 + if [[ -z "$event_path" ]]; then + log_message "警告: 事件路径为空,跳过此行" + continue + fi + + # 检查设备是否存在 + if [[ ! -e "$event_path" ]]; then + log_message "警告: 设备路径 $event_path 不存在,跳过" + continue + fi + + # 使用udevadm查询设备信息 + vid="" + pid="" + usb_path="" + + # 获取设备信息 + device_info=$(udevadm info --query=property --path="$(udevadm info --query=path --name="$event_path" 2>/dev/null)" 2>/dev/null) + + if [[ -n "$device_info" ]]; then + # 提取VID和PID + vid=$(echo "$device_info" | grep -i "ID_VENDOR_ID" | cut -d= -f2 | head -n1) + pid=$(echo "$device_info" | grep -i "ID_MODEL_ID" | cut -d= -f2 | head -n1) + + # 提取USB路径 + usb_path=$(echo "$device_info" | grep -i "ID_PATH" | grep -i "usb" | cut -d= -f2 | head -n1) + + # # 如果没找到USB路径,尝试其他方式 + # if [[ -z "$usb_path" ]]; then + # usb_path=$(echo "$device_info" | grep -i "DEVPATH" | cut -d= -f2 | head -n1) + # fi + fi + + + log_message "获取到信息: VID=$vid, PID=$pid, USB路径=$usb_path" + + # 组合新格式并写入输出文件 + new_line="${display_name}|${touchscreen_name}|${vid}|${pid}|${usb_path}" + echo "$new_line" >> "$OUTPUT_FILE" + log_message "写入: $new_line" + +done < "$INPUT_FILE" + +# 清空UOS的触摸屏设置信息 +if [ -f /usr/bin/gsettings ];then + gsettings set com.deepin.dde.display map-output ''> /dev/null 2>&1 + if [ "$?" == "0" ];then + log_message "成功清除UOS系统设置中的触摸屏设置信息" + fi +fi + +# 配置文件更新完成,进行一次试用 +log_message "开始进行触摸效果测试,并进行二次确认。" +$workpath/screen_ds_once $tmppath/screen.txt +$workpath/touch_set + +sleep 0.5 + +# 试用后询问用户是否保存 +$workpath/check_save +if [ ! "$?" = "0" ];then + # 用户点击了取消,恢复之前的设置 + log_message 用户点击了取消,恢复之前的设置. + rm $workpath/config + mv $workpath/config.bk $workpath/config + + # 最后写入0重新开启监控 + echo -n "0" > $tmppath/setflag + + notify-send -u low --icon=view-fullscreen -t 15000 "取消保存,已恢复之前的配置。" +else + log_message 用户点击了确认,新的config将会保存 + + # 最后写入0重新开启监控 + echo -n "0" > $tmppath/setflag + + log_message "处理完成,结果已保存到 $OUTPUT_FILE" + notify-send -u low --icon=view-fullscreen -t 15000 "触摸屏映射工具 ktouch" "设置完成,请稍等15秒查看效果。" +fi + + if [ -f $workpath/config.bk ];then + rm $workpath/config.bk + fi + diff --git a/opt/ktouch/src/Makefile b/opt/ktouch/src/Makefile new file mode 100644 index 0000000..64f3cdb --- /dev/null +++ b/opt/ktouch/src/Makefile @@ -0,0 +1,53 @@ +# 定义编译器 +CC = gcc + +# 定义pkg-config参数 +GTK_FLAGS = $(shell pkg-config --cflags --libs gtk+-3.0) + +# 定义目标文件 +TARGETS = screen_binder screen_ds touch_ds usb_ds touch_set screen_ds_once check_save + +# 默认目标 +all: $(TARGETS) + +# 编译screen_binder +screen_binder: screen_binder.c touch_listen.c + $(CC) -o $@ $^ $(GTK_FLAGS) -lpthread -lX11 -lXrandr + install -m 755 $@ ../ + +# 编译screen_ds +screen_ds: screen_ds.c + $(CC) -o $@ $^ -lX11 -lXrandr + install -m 755 $@ ../ + +# 编译screen_ds_once +screen_ds_once: screen_ds_once.c + $(CC) -o $@ $^ -lX11 -lXrandr + install -m 755 $@ ../ + +# 编译touch_set +touch_set: touch_set.c + $(CC) -o $@ $^ -lX11 -lXi -ludev + install -m 755 $@ ../ + +# 编译touch_ds +touch_ds: touch_ds.c + $(CC) -o $@ $^ -lX11 -lXi -ludev + install -m 755 $@ ../ + +# 编译usb_ds +usb_ds: usb_ds.c + $(CC) -o $@ $^ -ludev + install -m 755 $@ ../ + +# 编译check_save +check_save: check_save.c + $(CC) -o $@ $^ $(GTK_FLAGS) + install -m 755 $@ ../ + +# 清理生成的文件 +clean: + rm -f $(TARGETS) + +# 声明伪目标 +.PHONY: all clean \ No newline at end of file diff --git a/opt/ktouch/src/check_save.c b/opt/ktouch/src/check_save.c new file mode 100644 index 0000000..578d95d --- /dev/null +++ b/opt/ktouch/src/check_save.c @@ -0,0 +1,217 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + GtkWidget *window; + int monitor_num; + int x; + int y; + int width; + int height; + char *name; +} MonitorInfo; + +MonitorInfo *monitors = NULL; +int monitor_count = 0; +GtkApplication *app; +int exit_code = 1; // 默认退出代码为1(取消) + +// 显示错误对话框 +void show_error_dialog(const char *message) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "%s", message); + gtk_window_set_title(GTK_WINDOW(dialog), "错误"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +// 关闭所有窗口 +void close_all_windows() { + for (int i = 0; i < monitor_count; i++) { + if (monitors[i].window) { + gtk_widget_destroy(monitors[i].window); + } + } +} + +// 保存按钮点击回调函数 +void on_save_clicked(GtkWidget *widget, gpointer data) { + printf("0\n"); // 打印0 + exit_code = 0; // 设置退出代码为0 + + close_all_windows(); + g_application_quit(G_APPLICATION(app)); +} + +// 取消按钮点击回调函数 +void on_cancel_clicked(GtkWidget *widget, gpointer data) { + printf("1\n"); // 打印1 + exit_code = 1; // 设置退出代码为1 + + close_all_windows(); + g_application_quit(G_APPLICATION(app)); +} + +// 窗口关闭事件回调函数 +gboolean on_window_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data) { + printf("1\n"); // 打印1 + exit_code = 1; // 设置退出代码为1 + + close_all_windows(); + g_application_quit(G_APPLICATION(app)); + + return TRUE; // 阻止默认关闭行为,因为我们自己处理 +} + +// 创建显示器确认窗口 +void create_monitor_window(MonitorInfo *info) { + // 窗口尺寸 + int window_width = 400; + int window_height = 200; + + // 计算居中位置 + int center_x = info->x + (info->width - window_width) / 2; + int center_y = info->y + (info->height - window_height) / 2; + + // 创建窗口标题,包含显示器名称 + char window_title[256]; + if (info->name && strlen(info->name) > 0) { + snprintf(window_title, sizeof(window_title), "ktouch保存确认-%s", info->name); + } else { + snprintf(window_title, sizeof(window_title), "ktouch保存确认-显示器%d", info->monitor_num + 1); + } + + // 创建窗口 + info->window = gtk_application_window_new(app); + gtk_window_set_title(GTK_WINDOW(info->window), window_title); + gtk_window_set_default_size(GTK_WINDOW(info->window), window_width, window_height); + gtk_window_move(GTK_WINDOW(info->window), center_x, center_y); + gtk_window_set_keep_above(GTK_WINDOW(info->window), TRUE); + gtk_window_set_decorated(GTK_WINDOW(info->window), TRUE); + gtk_window_set_position(GTK_WINDOW(info->window), GTK_WIN_POS_CENTER); + + // 连接窗口关闭事件 + g_signal_connect(info->window, "delete-event", G_CALLBACK(on_window_delete_event), NULL); + + // 创建容器 + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_container_set_border_width(GTK_CONTAINER(box), 20); + gtk_container_add(GTK_CONTAINER(info->window), box); + + // 提示文字 + GtkWidget *label = gtk_label_new("是否保存刚刚的配置的信息?\n任意窗口都可以操作。"); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); + + // 按钮容器(水平排列) + GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_set_homogeneous(GTK_BOX(button_box), TRUE); + gtk_box_pack_start(GTK_BOX(box), button_box, FALSE, FALSE, 0); + + // 保存按钮 + GtkWidget *save_button = gtk_button_new_with_label("保存"); + g_signal_connect(save_button, "clicked", G_CALLBACK(on_save_clicked), NULL); + gtk_box_pack_start(GTK_BOX(button_box), save_button, TRUE, TRUE, 0); + + // 取消按钮 + GtkWidget *cancel_button = gtk_button_new_with_label("取消并退出"); + g_signal_connect(cancel_button, "clicked", G_CALLBACK(on_cancel_clicked), NULL); + gtk_box_pack_start(GTK_BOX(button_box), cancel_button, TRUE, TRUE, 0); + + // 显示窗口 + gtk_widget_show_all(info->window); +} + +// 应用启动回调函数 +static void activate(GApplication *application, gpointer user_data) { + GdkScreen *screen = gdk_screen_get_default(); + if (!screen) { + fprintf(stderr, "无法获取屏幕信息\n"); + show_error_dialog("无法获取屏幕信息"); + exit(1); + } + + // 获取显示器数量 - 使用兼容旧版本的方法 + monitor_count = gdk_screen_get_n_monitors(screen); + if (monitor_count <= 0) { + fprintf(stderr, "未找到显示器\n"); + show_error_dialog("未找到显示器"); + exit(1); + } + + printf("找到 %d 个显示器\n", monitor_count); + + // 分配内存存储显示器信息 + monitors = malloc(monitor_count * sizeof(MonitorInfo)); + memset(monitors, 0, monitor_count * sizeof(MonitorInfo)); + + // 收集所有显示器信息 + for (int i = 0; i < monitor_count; i++) { + GdkRectangle geometry; + gdk_screen_get_monitor_geometry(screen, i, &geometry); + + monitors[i].monitor_num = i; + monitors[i].x = geometry.x; + monitors[i].y = geometry.y; + monitors[i].width = geometry.width; + monitors[i].height = geometry.height; + + // 尝试获取显示器名称/接口 + const char *monitor_name = gdk_screen_get_monitor_plug_name(screen, i); + if (monitor_name && strlen(monitor_name) > 0) { + monitors[i].name = strdup(monitor_name); + } else { + // 如果无法获取显示器名称,使用默认名称 + char default_name[50]; + snprintf(default_name, sizeof(default_name), "显示器%d", i + 1); + monitors[i].name = strdup(default_name); + } + + printf("显示器 %d: 位置(%d, %d), 分辨率 %dx%d, 名称: %s\n", + i+1, geometry.x, geometry.y, geometry.width, geometry.height, + monitors[i].name); + } + + // 为每个显示器创建窗口 + for (int i = 0; i < monitor_count; i++) { + create_monitor_window(&monitors[i]); + } +} + +int main(int argc, char *argv[]) { + // 检查DISPLAY环境变量,确保图形环境可用 + if (getenv("DISPLAY") == NULL) { + fprintf(stderr, "DISPLAY环境变量未设置,尝试设置默认值\n"); + setenv("DISPLAY", ":0", 1); + } + + // 创建GTK应用 + app = gtk_application_new("com.example.touchscreen_confirm", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + + int status = g_application_run(G_APPLICATION(app), 0, NULL); + g_object_unref(app); + + // 释放内存 + if (monitors) { + for (int i = 0; i < monitor_count; i++) { + if (monitors[i].name) { + free(monitors[i].name); + } + } + free(monitors); + } + + // 返回相应的退出代码 + return exit_code; +} \ No newline at end of file diff --git a/opt/ktouch/src/demo.c b/opt/ktouch/src/demo.c new file mode 100644 index 0000000..916794b --- /dev/null +++ b/opt/ktouch/src/demo.c @@ -0,0 +1,395 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 添加Xrandr头文件和X11显示类型头文件 +#ifdef GDK_WINDOWING_X11 +#include +#include +#endif + +int win_time_delay = 50; + +// 全局变量 +GtkWidget **info_windows = NULL; +int total_screens = 0; +int *screen_widths = NULL; +int *screen_heights = NULL; +int *screen_x_offsets = NULL; +int *screen_y_offsets = NULL; +char **screen_names = NULL; +FILE *log_file = NULL; +int current_test_round = 0; +int total_test_rounds = 0; +int space_pressed = 0; +guint create_timeout_id = 0; +int current_window_index = 0; + +// 函数声明 +void get_screen_info(); +void log_message(const char *format, ...); +GtkWidget* create_info_window(int screen_index); +gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data); +gboolean create_next_window(gpointer user_data); +void close_all_windows(); +void verify_windows(); +void start_next_test_round(); +void on_info_window_destroy(GtkWidget *widget, gpointer user_data); + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + printf("[%s] screen_demo \t", time_str); + vprintf(format, args); + + if (log_file) { + fprintf(log_file, "[%s] screen_demo \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); + } + + va_end(args); +} + +// 获取屏幕信息 (使用Xrandr方法) +void get_screen_info() { + GdkDisplay *gdk_display = gdk_display_get_default(); + + #ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY(gdk_display)) { + log_message("错误: 非X11显示环境\n"); + return; + } + + Display *xdisplay = GDK_DISPLAY_XDISPLAY(gdk_display); + Window xroot = GDK_WINDOW_XID(gdk_get_default_root_window()); + + int rr_event_base, rr_error_base; + if (!XRRQueryExtension(xdisplay, &rr_event_base, &rr_error_base)) { + log_message("错误: XRandR扩展不可用\n"); + return; + } + + XRRScreenResources *screen_res = XRRGetScreenResourcesCurrent(xdisplay, xroot); + if (!screen_res) { + log_message("错误: 无法获取屏幕资源\n"); + return; + } + + // 计算连接的显示器数量 + total_screens = 0; + for (int i = 0; i < screen_res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + total_screens++; + } + if (output_info) XRRFreeOutputInfo(output_info); + } + + log_message("找到 %d 个连接的显示器\n", total_screens); + + // 分配内存存储屏幕信息 + screen_widths = malloc(total_screens * sizeof(int)); + screen_heights = malloc(total_screens * sizeof(int)); + screen_x_offsets = malloc(total_screens * sizeof(int)); + screen_y_offsets = malloc(total_screens * sizeof(int)); + screen_names = malloc(total_screens * sizeof(char*)); + info_windows = malloc(total_screens * sizeof(GtkWidget*)); + + // 获取每个显示器的详细信息 + int screen_index = 0; + for (int i = 0; i < screen_res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + if (output_info->crtc) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(xdisplay, screen_res, output_info->crtc); + if (crtc_info) { + screen_names[screen_index] = strdup(output_info->name); + screen_widths[screen_index] = crtc_info->width; + screen_heights[screen_index] = crtc_info->height; + screen_x_offsets[screen_index] = crtc_info->x; + screen_y_offsets[screen_index] = crtc_info->y; + + info_windows[screen_index] = NULL; + + log_message("显示器 %d: %s, 分辨率: %dx%d, 位置: %dx%d\n", + screen_index, screen_names[screen_index], + screen_widths[screen_index], screen_heights[screen_index], + screen_x_offsets[screen_index], screen_y_offsets[screen_index]); + + XRRFreeCrtcInfo(crtc_info); + screen_index++; + } + } + } + if (output_info) XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(screen_res); + #else + log_message("错误: 非X11环境,无法使用Xrandr\n"); + #endif +} + +// 创建信息窗口 +GtkWidget* create_info_window(int screen_index) { + // 计算窗口位置和大小:偏移(10,10),尺寸比屏幕小20 + int window_x = screen_x_offsets[screen_index] + 10; + int window_y = screen_y_offsets[screen_index] + 10; + int window_width = screen_widths[screen_index] - 20; + int window_height = screen_heights[screen_index] - 20; + + // 确保窗口尺寸不会为负值 + if (window_width < 100) window_width = 100; + if (window_height < 100) window_height = 100; + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "屏幕信息演示"); + gtk_window_move(GTK_WINDOW(window), window_x, window_y); + gtk_window_set_default_size(GTK_WINDOW(window), window_width, window_height); + gtk_window_set_decorated(GTK_WINDOW(window), TRUE); + gtk_window_set_keep_above(GTK_WINDOW(window), TRUE); + + char message[512]; + snprintf(message, sizeof(message), + "屏幕 %d: %s\n\n" + "分辨率: %dx%d\n" + "位置: (%d, %d)\n\n" + "窗口位置: (%d, %d)\n" + "窗口大小: %dx%d\n\n" + "测试轮次: %d/%d\n\n" + "按空格键继续...", + screen_index, screen_names[screen_index], + screen_widths[screen_index], screen_heights[screen_index], + screen_x_offsets[screen_index], screen_y_offsets[screen_index], + window_x, window_y, window_width, window_height, + current_test_round + 1, total_test_rounds); + + GtkWidget *label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), message); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + + // 添加容器使标签居中 + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(window), box); + + g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL); + g_signal_connect(window, "destroy", G_CALLBACK(on_info_window_destroy), GINT_TO_POINTER(screen_index)); + + return window; +} + +// 键盘事件处理 +gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { + if (event->keyval == GDK_KEY_space) { + log_message("用户按下空格键\n"); + space_pressed = 1; + + // 关闭所有窗口并开始下一轮测试 + close_all_windows(); + return TRUE; + } + return FALSE; +} + +// 信息窗口销毁事件处理 +void on_info_window_destroy(GtkWidget *widget, gpointer user_data) { + int screen_index = GPOINTER_TO_INT(user_data); + log_message("屏幕 %d (%s) 的信息窗口已销毁\n", screen_index, screen_names[screen_index]); + info_windows[screen_index] = NULL; +} + +// 创建下一个窗口(间隔3秒) +gboolean create_next_window(gpointer user_data) { + if (current_window_index >= total_screens) { + log_message("所有窗口已创建完成\n"); + verify_windows(); + create_timeout_id = 0; + return G_SOURCE_REMOVE; + } + + log_message("正在为屏幕 %d (%s) 创建信息窗口...\n", + current_window_index, screen_names[current_window_index]); + + info_windows[current_window_index] = create_info_window(current_window_index); + gtk_widget_show_all(info_windows[current_window_index]); + + log_message("屏幕 %d 的信息窗口已显示\n", current_window_index); + + current_window_index++; + + if (current_window_index < total_screens) { + // 设置3秒后创建下一个窗口 + // create_timeout_id = g_timeout_add_seconds(3, create_next_window, NULL); + create_timeout_id = g_timeout_add(win_time_delay, create_next_window, NULL); + } else { + log_message("所有窗口已创建完成\n"); + verify_windows(); + } + + return G_SOURCE_REMOVE; +} + +// 关闭所有窗口 +void close_all_windows() { + log_message("正在关闭所有窗口...\n"); + + for (int i = 0; i < total_screens; i++) { + if (info_windows[i] != NULL) { + gtk_widget_destroy(info_windows[i]); + info_windows[i] = NULL; + } + } + + // 取消超时计时器 + if (create_timeout_id > 0) { + g_source_remove(create_timeout_id); + create_timeout_id = 0; + } + + log_message("所有窗口已关闭\n"); + + // 开始下一轮测试 + start_next_test_round(); +} + +// 校验窗口的显示状态、位置和大小 +void verify_windows() { + log_message("开始校验窗口状态...\n"); + + int all_windows_ok = 1; + + for (int i = 0; i < total_screens; i++) { + if (info_windows[i] == NULL) { + log_message("错误: 屏幕 %d 的窗口未创建\n", i); + all_windows_ok = 0; + continue; + } + + if (!gtk_widget_get_visible(info_windows[i])) { + log_message("错误: 屏幕 %d 的窗口不可见\n", i); + all_windows_ok = 0; + } + + // 获取窗口实际位置和大小 + int x, y, width, height; + gtk_window_get_position(GTK_WINDOW(info_windows[i]), &x, &y); + gtk_window_get_size(GTK_WINDOW(info_windows[i]), &width, &height); + + // 计算期望的位置和大小 + int expected_x = screen_x_offsets[i] + 10; + int expected_y = screen_y_offsets[i] + 10; + int expected_width = screen_widths[i] - 20; + int expected_height = screen_heights[i] - 20; + + // 确保期望尺寸不会为负值 + if (expected_width < 100) expected_width = 100; + if (expected_height < 100) expected_height = 100; + + // 校验位置(允许±5像素的误差) + if (abs(x - expected_x) > 5 || abs(y - expected_y) > 5) { + log_message("警告: 屏幕 %d 的窗口位置不匹配。预期: (%d, %d), 实际: (%d, %d)\n", + i, expected_x, expected_y, x, y); + } + + // 校验大小(允许±5像素的误差) + if (abs(width - expected_width) > 5 || abs(height - expected_height) > 5) { + log_message("警告: 屏幕 %d 的窗口大小不匹配。预期: %dx%d, 实际: %dx%d\n", + i, expected_width, expected_height, width, height); + } + + log_message("屏幕 %d 校验完成: 位置=(%d, %d), 大小=%dx%d\n", i, x, y, width, height); + } + + if (all_windows_ok) { + log_message("所有窗口校验成功\n"); + } else { + log_message("部分窗口校验失败\n"); + } +} + +// 开始下一轮测试 +void start_next_test_round() { + current_test_round++; + + if (current_test_round >= total_test_rounds) { + log_message("所有测试轮次已完成,程序结束\n"); + + // 清理资源 + for (int i = 0; i < total_screens; i++) { + free(screen_names[i]); + } + free(screen_widths); + free(screen_heights); + free(screen_x_offsets); + free(screen_y_offsets); + free(screen_names); + free(info_windows); + + // 关闭日志文件 + if (log_file) { + fclose(log_file); + log_file = NULL; + } + + gtk_main_quit(); + return; + } + + log_message("开始第 %d/%d 轮测试\n", current_test_round + 1, total_test_rounds); + + // 重置窗口索引 + current_window_index = 0; + space_pressed = 0; + + // 开始创建窗口(第一个窗口立即创建,后续窗口间隔3秒) + create_timeout_id = g_timeout_add(100, create_next_window, NULL); +} + +int main(int argc, char *argv[]) { + log_file = fopen("screen_demo.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + log_message("=== 多屏幕信息窗口演示程序 ===\n"); + + gtk_init(&argc, &argv); + + // 获取屏幕信息 + get_screen_info(); + + if (total_screens == 0) { + log_message("未找到任何屏幕,程序退出\n"); + return 1; + } + + // 设置测试轮次为屏幕数量 + total_test_rounds = total_screens; + log_message("将执行 %d 轮测试(每轮在所有屏幕上创建窗口)\n", total_test_rounds); + + // 开始第一轮测试 + current_test_round = -1; + start_next_test_round(); + + gtk_main(); + + log_message("程序结束\n"); + return 0; +} \ No newline at end of file diff --git a/opt/ktouch/src/display_control.py b/opt/ktouch/src/display_control.py new file mode 100644 index 0000000..974669b --- /dev/null +++ b/opt/ktouch/src/display_control.py @@ -0,0 +1,677 @@ +#!/usr/bin/env python3 +import dbus +import logging +import time +import os +from typing import List, Dict, Any +import sys + +setting_file_dir="/opt/ktouch/display_config" + + +class DisplayControl: + def __init__(self): + self.logger = logging.getLogger('DisplayControl') + self.logger.setLevel(logging.INFO) + + # 创建控制台处理器 + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch.setFormatter(formatter) + self.logger.addHandler(ch) + + self.bus = None + self.display_obj = None + self.display_props = None + self.display_interface = None + + self.connect_to_dbus() + + def connect_to_dbus(self): + """连接到DBus服务""" + try: + self.bus = dbus.SessionBus() + self.display_obj = self.bus.get_object('com.deepin.daemon.Display', '/com/deepin/daemon/Display') + self.display_props = dbus.Interface(self.display_obj, 'org.freedesktop.DBus.Properties') + self.display_interface = dbus.Interface(self.display_obj, 'com.deepin.daemon.Display') + self.logger.info("成功连接到DBus显示服务") + except Exception as e: + self.logger.error(f"连接DBus显示服务失败: {str(e)}") + raise + + # def get_monitors(self) -> List[Dict[str, Any]]: + # """获取所有显示器信息""" + # self.logger.info("开始获取显示器信息") + # try: + # monitors_paths = self.display_props.Get('com.deepin.daemon.Display', 'Monitors') + # self.logger.info(f"找到 {len(monitors_paths)} 个显示器") + + # monitors = [] + # for monitor_path in monitors_paths: + # monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + # monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties') + + # # 获取显示器属性 + # name = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Name')) + # enabled = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Enabled')) + # modes = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Modes') + # current_mode = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'CurrentMode') + # x = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'X')) + # y = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Y')) + # width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width')) + # height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height')) + # rotation = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Rotation')) + + # # 获取主显示器 + # primary = self.display_props.Get('com.deepin.daemon.Display', 'Primary') + # is_primary = (primary == name) + + # # 检查是否存在配置文件 + # config_exists = self.check_config_exists(name) + + # # 解析可用模式,过滤出60fps的模式 + # available_modes = [] + # for mode in modes: + # mode_id = int(mode[0]) + # mode_width = int(mode[1]) + # mode_height = int(mode[2]) + # refresh_rate = int(mode[3]) + + # # 过滤55-65Hz的刷新率 + # if 55 <= refresh_rate <= 65: + # available_modes.append({ + # 'id': mode_id, + # 'width': mode_width, + # 'height': mode_height, + # 'refresh_rate': refresh_rate, + # 'label': f"{mode_width}x{mode_height} @ {refresh_rate:.2f}Hz" + # }) + + # # 添加自定义分辨率选项 + # available_modes.append({ + # 'id': 'custom', + # 'width': width, # 默认使用当前宽度 + # 'height': height, # 默认使用当前高度 + # 'refresh_rate': 60.0, # 默认刷新率 + # 'label': "自定义分辨率" + # }) + + # # 如果有配置文件,设置current_mode为id=0 + # if config_exists: + # current_mode = [0, width, height, 60] # id=0表示自定义模式 + # self.logger.info(f"显示器 {name} 使用自定义分辨率模式") + + # monitors.append({ + # 'path': monitor_path, + # 'name': name, + # 'enabled': enabled, + # 'x': x, + # 'y': y, + # 'width': width, + # 'height': height, + # 'rotation': rotation, + # 'is_primary': is_primary, + # 'current_mode': [int(current_mode[0]), int(current_mode[1]), int(current_mode[2]), int(current_mode[3])], + # 'modes': available_modes, + # 'custom_width': width, # 自定义宽度 + # 'custom_height': height, # 自定义高度 + # 'custom_refresh_rate': 60.0, # 自定义刷新率 + # 'has_custom_config': config_exists # 是否有自定义配置 + # }) + # self.logger.info(f"显示器: {name}, 启用: {enabled}, 位置: ({x}, {y}), 分辨率: {width}x{height}, 旋转: {rotation}, 主屏: {is_primary}") + + # # 估算显示器的位置顺序(基于X坐标) + # self.estimate_monitor_positions(monitors) + + # self.logger.info("显示器信息获取完成") + # return monitors + + # except Exception as e: + # self.logger.error(f"获取显示器信息时出错: {str(e)}") + # raise + + + def get_monitors(self) -> List[Dict[str, Any]]: + """获取所有显示器信息""" + self.logger.info("开始获取显示器信息") + try: + monitors_paths = self.display_props.Get('com.deepin.daemon.Display', 'Monitors') + self.logger.info(f"找到 {len(monitors_paths)} 个显示器") + + monitors = [] + for monitor_path in monitors_paths: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties') + + # 获取显示器基础属性 + name = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Name')) + enabled = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Enabled')) + modes = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Modes') + x = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'X')) + y = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Y')) + width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width')) + height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height')) + rotation = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Rotation')) + + # 检查是否存在配置文件并加载 + config_exists = self.check_config_exists(name) + custom_config = None + if config_exists: + custom_config = self.load_custom_resolution(name) + if custom_config: + # 从配置文件更新分辨率信息 + width = custom_config['width'] + height = custom_config['height'] + self.logger.info(f"显示器 {name} 加载配置文件中的分辨率: {width}x{height}") + else: + self.logger.warning(f"显示器 {name} 配置文件存在但解析失败,将使用系统分辨率") + + # 确定当前模式(优先使用配置文件信息) + if config_exists and custom_config: + # 配置文件存在且有效时,使用配置的分辨率,模式ID为0,刷新率60 + current_mode = [0, width, height, 60] + self.logger.info(f"显示器 {name} 使用配置文件中的当前模式: {current_mode}") + else: + # 否则使用系统当前模式 + current_mode = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'CurrentMode') + current_mode = [int(current_mode[0]), int(current_mode[1]), int(current_mode[2]), int(current_mode[3])] + + # 获取主显示器信息 + primary = self.display_props.Get('com.deepin.daemon.Display', 'Primary') + is_primary = (primary == name) + + # 解析可用模式,过滤出55-65Hz的刷新率 + available_modes = [] + for mode in modes: + mode_id = int(mode[0]) + mode_width = int(mode[1]) + mode_height = int(mode[2]) + refresh_rate = int(mode[3]) + + if 55 <= refresh_rate <= 65: + available_modes.append({ + 'id': mode_id, + 'width': mode_width, + 'height': mode_height, + 'refresh_rate': refresh_rate, + 'label': f"{mode_width}x{mode_height} @ {refresh_rate:.2f}Hz" + }) + + # 添加自定义分辨率选项(使用当前有效分辨率作为默认值) + available_modes.append({ + 'id': 'custom', + 'width': width, + 'height': height, + 'refresh_rate': 60.0, + 'label': "自定义分辨率" + }) + + monitors.append({ + 'path': monitor_path, + 'name': name, + 'enabled': enabled, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'rotation': rotation, + 'is_primary': is_primary, + 'current_mode': current_mode, + 'modes': available_modes, + 'custom_width': width, + 'custom_height': height, + 'custom_refresh_rate': 60.0, + 'has_custom_config': config_exists + }) + self.logger.info(f"显示器: {name}, 启用: {enabled}, 位置: ({x}, {y}), 分辨率: {width}x{height}, 旋转: {rotation}, 主屏: {is_primary}") + + # 估算显示器的位置顺序(基于X坐标) + self.estimate_monitor_positions(monitors) + + self.logger.info("显示器信息获取完成") + return monitors + + except Exception as e: + self.logger.error(f"获取显示器信息时出错: {str(e)}") + raise + + def check_config_exists(self, monitor_name: str) -> bool: + """检查显示器配置文件是否存在""" + config_dir = os.path.expanduser(setting_file_dir) + config_file = os.path.join(config_dir, f"{monitor_name}.conf") + return os.path.exists(config_file) + + def estimate_monitor_positions(self, monitors: List[Dict[str, Any]]): + """根据X坐标估算显示器的位置顺序""" + # 按X坐标排序 + sorted_monitors = sorted(monitors, key=lambda m: m['x']) + + seq = 0 + last_x = -1 + # 分配位置编号 + for i, monitor in enumerate(sorted_monitors): + if monitor['x'] == last_x: + monitor['position'] = seq + last_x = int(monitor['x']) + else: + seq += 1 + monitor['position'] = seq + last_x = int(monitor['x']) + self.logger.info(f"显示器 {monitor['name']} 估算位置: {monitor['position']} (X坐标: {monitor['x']})") + + def enable_monitor(self, monitor_path: str, enabled: bool): + """启用或禁用显示器""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + if enabled: + self.logger.info(f"启用显示器: {monitor_path}") + monitor_interface.Enable() + else: + self.logger.info(f"禁用显示器: {monitor_path}") + monitor_interface.Disable() + + except Exception as e: + self.logger.error(f"设置显示器启用状态时出错: {str(e)}") + raise + + def set_mode(self, monitor_path: str, mode_id: int, monitor_name: str = None): + """通过Mode ID设置显示器模式""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + self.logger.info(f"设置显示器 {monitor_path} 的Mode ID为 {mode_id}") + monitor_interface.SetMode(mode_id) + + # 如果是系统模式,删除配置文件 + if mode_id != 'custom' and monitor_name: + self.delete_config(monitor_name) + + except Exception as e: + self.logger.error(f"设置显示器Mode时出错: {str(e)}") + raise + + # 设置显示模式为填充 + try: + monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties') + monitor_props.Set('com.deepin.daemon.Display.Monitor', 'CurrentFillMode', dbus.String('Full')) + self.logger.info(f"已设置显示器 {monitor_path} 的CurrentFillMode为Full") + except Exception as fill_mode_error: + self.logger.warning(f"设置CurrentFillMode失败: {str(fill_mode_error)}") + + def set_mode_by_size(self, monitor_path: str, width: int, height: int, monitor_name: str = None, is_custom: bool = False): + """通过分辨率设置显示器模式""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + self.logger.info(f"设置显示器 {monitor_path} 的分辨率为 {width}x{height}") + monitor_interface.SetModeBySize(width, height) + + # 如果是自定义分辨率,保存配置并创建need_update文件 + if is_custom and monitor_name: + self.save_custom_resolution(monitor_name, width, height, 60.0) + self.create_need_update_file() + # 如果是系统模式,删除配置文件 + elif monitor_name: + self.delete_config(monitor_name) + + except Exception as e: + self.logger.error(f"设置显示器分辨率时出错: {str(e)}") + raise + + try: + monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties') + monitor_props.Set('com.deepin.daemon.Display.Monitor', 'CurrentFillMode', dbus.String('Full')) + self.logger.info(f"已设置显示器 {monitor_path} 的CurrentFillMode为Full") + except Exception as fill_mode_error: + self.logger.warning(f"设置CurrentFillMode失败: {str(fill_mode_error)}") + + + def set_position(self, monitor_path: str, x: int, y: int): + """设置显示器位置""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + self.logger.info(f"设置显示器 {monitor_path} 的位置为 ({x}, {y})") + monitor_interface.SetPosition(x, y) + + except Exception as e: + self.logger.error(f"设置显示器位置时出错: {str(e)}") + raise + + def set_refresh_rate(self, monitor_path: str, refresh_rate: float, monitor_name: str = None, is_custom: bool = False): + """设置显示器刷新率""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + self.logger.info(f"设置显示器 {monitor_path} 的刷新率为 {refresh_rate}Hz") + monitor_interface.SetRefreshRate(refresh_rate) + + # 如果是自定义刷新率,保存配置并创建need_update文件 + if is_custom and monitor_name: + # 需要先获取当前分辨率 + monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties') + width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width')) + height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height')) + self.save_custom_resolution(monitor_name, width, height, refresh_rate) + self.create_need_update_file() + # 如果是系统模式,删除配置文件 + elif monitor_name: + self.delete_config(monitor_name) + + except Exception as e: + self.logger.error(f"设置显示器刷新率时出错: {str(e)}") + raise + + def set_rotation(self, monitor_path: str, rotation: int): + """设置显示器旋转方向""" + try: + monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path) + monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor') + + rotation_names = { + 1: "正常", + 2: "向左90度", + 4: "翻转", + 8: "向右90度" + } + self.logger.info(f"设置显示器 {monitor_path} 的旋转方向为 {rotation} ({rotation_names.get(rotation, '未知')})") + monitor_interface.SetRotation(rotation) + + except Exception as e: + self.logger.error(f"设置显示器旋转方向时出错: {str(e)}") + raise + + + def switch_mode(self): + """切换显示模式,为设置主显示器做准备""" + try: + self.logger.info("调用SwitchMode方法准备设置主显示器") + # 调用SwitchMode方法,第一个参数是byte类型的2,第二个参数是空字符串 + self.display_interface.SwitchMode(dbus.Byte(2), dbus.String("")) + self.logger.info("SwitchMode方法调用成功") + except Exception as e: + self.logger.error(f"调用SwitchMode方法时出错: {str(e)}") + raise + + def set_primary(self, monitor_name: str): + """设置主显示器""" + try: + self.logger.info(f"设置主显示器: {monitor_name}") + self.display_interface.SetPrimary(monitor_name) + + except Exception as e: + self.logger.error(f"设置主显示器时出错: {str(e)}") + raise + + def apply_changes(self): + """应用更改""" + try: + self.logger.info("应用显示设置更改") + self.display_interface.ApplyChanges() + + except Exception as e: + self.logger.error(f"应用显示设置时出错: {str(e)}") + raise + + def save_settings(self): + """保存设置""" + try: + self.logger.info("保存显示设置") + self.display_interface.Save() + + except Exception as e: + self.logger.error(f"保存显示设置时出错: {str(e)}") + raise + + def save_custom_resolution(self, monitor_name: str, config: str): + """保存自定义配置到文件,配置格式为“{width}x{height}@{rate}|x,y|rotation|isPrimary”""" + try: + config_dir = os.path.expanduser(setting_file_dir) + os.makedirs(config_dir, exist_ok=True) + + config_file = os.path.join(config_dir, f"{monitor_name}.conf") + + with open(config_file, 'w') as f: + f.write(config) + + self.logger.info(f"已保存自定义配置到 {config_file}: {config}") + + except Exception as e: + self.logger.error(f"保存自定义配置时出错: {str(e)}") + + def remove_custom_resolution(self, monitor_name): + config_dir = os.path.expanduser(setting_file_dir) + os.makedirs(config_dir, exist_ok=True) + + config_file = os.path.join(config_dir, f"{monitor_name}.conf") + + if os.path.exists(config_file): + try: + # 删除配置文件 + os.remove(config_file) + # logging.info(f"已删除显示器 {monitor_name} 的自定义分辨率配置文件") + return True + except Exception as e: + # logging.error(f"删除显示器 {monitor_name} 的自定义分辨率配置文件时出错: {str(e)}") + return False + + return True + + + + def create_need_update_file(self): + """创建need_update文件,如果打开失败则再尝试一次""" + try: + config_dir = os.path.expanduser(setting_file_dir) + os.makedirs(config_dir, exist_ok=True) + + need_update_file = os.path.join(config_dir, "need_update") + + try: + with open(need_update_file, 'w') as f: + f.write('1') + except: + # 如果失败,再尝试一次 + try: + with open(need_update_file, 'w') as f: + f.write('1') + except Exception as e: + self.logger.error(f"写入need_update文件失败: {str(e)}") + + except Exception as e: + self.logger.error(f"创建need_update文件时出错: {str(e)}") + + def delete_config(self, monitor_name: str): + """删除配置文件""" + try: + config_dir = os.path.expanduser(setting_file_dir) + config_file = os.path.join(config_dir, f"{monitor_name}.conf") + + if os.path.exists(config_file): + os.remove(config_file) + self.logger.info(f"已删除配置文件: {config_file}") + + except Exception as e: + self.logger.error(f"删除配置文件时出错: {str(e)}") + + # def load_custom_resolution(self, monitor_name: str): + # """ + # 加载自定义分辨率配置文件 + + # 配置文件新格式: {width}x{height}@{rate}|x,y|rotation|isPrimary + # 例如: 1920x1080@60|1920,0|0|True + # 配置文件路径: 同目录下的display_settings目录,以显示器名称.conf命名 + + # Args: + # monitor_name: 显示器名称 + + # Returns: + # 包含解析后的配置信息的字典,解析失败返回None + # """ + # try: + # # 构建配置文件路径:同目录下的display_settings目录,文件名格式为"显示器名称.conf" + # import os + # # 获取当前文件所在目录 + # current_dir = os.path.dirname(os.path.abspath(__file__)) + # # 构建display_settings目录路径 + # settings_dir = setting_file_dir + # # 构建完整配置文件路径 + # config_path = os.path.join(settings_dir, f"{monitor_name}.conf") + + # # 检查配置文件目录是否存在 + # if not os.path.exists(settings_dir): + # self.logger.warning(f"配置文件目录不存在: {settings_dir}") + # return None + + # # 检查配置文件是否存在 + # if not os.path.exists(config_path): + # self.logger.warning(f"显示器 {monitor_name} 配置文件不存在: {config_path}") + # return None + + # with open(config_path, 'r', encoding='utf-8') as f: + # content = f.read().strip() + + # # 按分隔符分割配置项 + # parts = content.split('|') + # if len(parts) != 4: + # self.logger.error(f"显示器 {monitor_name} 配置文件格式错误,需要4个部分,实际有{len(parts)}个") + # return None + + # # 解析分辨率和刷新率部分 (格式: {width}x{height}@{rate}) + # resolution_part = parts[0].strip() + # if '@' not in resolution_part or 'x' not in resolution_part: + # self.logger.error(f"显示器 {monitor_name} 分辨率格式错误: {resolution_part}") + # return None + + # # 分离分辨率和刷新率 + # resolution, rate_str = resolution_part.split('@', 1) + # width_str, height_str = resolution.split('x', 1) + # rate_str = rate_str[0:-2] + # print(rate_str) + + # # 解析位置信息 (格式: x,y) + # position_part = parts[1].strip() + # x_str, y_str = position_part.split(',', 1) + + # # 解析旋转角度和主显示器标识 + # rotation_str = parts[2].strip() + # is_primary_str = parts[3].strip().lower() + + # # 转换为相应的数据类型 + # return { + # 'width': int(width_str), + # 'height': int(height_str), + # 'refresh_rate': float(rate_str), + # 'x': int(x_str), + # 'y': int(y_str), + # 'rotation': int(rotation_str), + # 'is_primary': is_primary_str in ('true', '1', 'yes') + # } + + # except ValueError as e: + # self.logger.error(f"显示器 {monitor_name} 配置文件数值解析失败: {str(e)}") + # except FileNotFoundError: + # self.logger.warning(f"显示器 {monitor_name} 配置文件未找到: {config_path}") + # except Exception as e: + # self.logger.error(f"加载显示器 {monitor_name} 配置文件时出错: {str(e)}") + + # return None + + + def load_custom_resolution(self, monitor_name: str): + """ + 加载自定义分辨率配置文件 + + 配置文件新格式: {width}x{height}@{rate}|x,y|rotation|isPrimary|forceFlag + 例如: 1920x1080@60|1920,0|0|True|F 或 1920x1080@60|1920,0|0|True|A + 配置文件路径: 同目录下的display_settings目录,以显示器名称.conf命名 + + Args: + monitor_name: 显示器名称 + + Returns: + 包含解析后的配置信息的字典,解析失败返回None + """ + try: + # 构建配置文件路径:同目录下的display_settings目录,文件名格式为"显示器名称.conf" + import os + # 获取当前文件所在目录 + current_dir = os.path.dirname(os.path.abspath(__file__)) + # 构建display_settings目录路径 + settings_dir = setting_file_dir + # 构建完整配置文件路径 + config_path = os.path.join(settings_dir, f"{monitor_name}.conf") + + # 检查配置文件目录是否存在 + if not os.path.exists(settings_dir): + self.logger.warning(f"配置文件目录不存在: {settings_dir}") + return None + + # 检查配置文件是否存在 + if not os.path.exists(config_path): + self.logger.warning(f"显示器 {monitor_name} 配置文件不存在: {config_path}") + return None + + with open(config_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + + # 按分隔符分割配置项,现在可能有4或5个部分(兼容旧格式) + parts = content.split('|') + if len(parts) < 4: + self.logger.error(f"显示器 {monitor_name} 配置文件格式错误,需要至少4个部分,实际有{len(parts)}个") + return None + + # 解析分辨率和刷新率部分 (格式: {width}x{height}@{rate}) + resolution_part = parts[0].strip() + if '@' not in resolution_part or 'x' not in resolution_part: + self.logger.error(f"显示器 {monitor_name} 分辨率格式错误: {resolution_part}") + return None + + # 分离分辨率和刷新率 + resolution, rate_str = resolution_part.split('@', 1) + width_str, height_str = resolution.split('x', 1) + rate_str = rate_str[0:-2] # 移除"Hz" + + # 解析位置信息 (格式: x,y) + position_part = parts[1].strip() + x_str, y_str = position_part.split(',', 1) + + # 解析旋转角度和主显示器标识 + rotation_str = parts[2].strip() + is_primary_str = parts[3].strip().lower() + + # 转换为相应的数据类型 + result = { + 'width': int(width_str), + 'height': int(height_str), + 'refresh_rate': float(rate_str), + 'x': int(x_str), + 'y': int(y_str), + 'rotation': int(rotation_str), + 'is_primary': is_primary_str in ('true', '1', 'yes', 'p') + } + + # 如果有第5个部分(强制标志),则解析它 + if len(parts) >= 5: + force_flag = parts[4].strip().upper() + result['force_custom'] = (force_flag == 'F') + else: + result['force_custom'] = False # 默认值 + + return result + + except ValueError as e: + self.logger.error(f"显示器 {monitor_name} 配置文件数值解析失败: {str(e)}") + except FileNotFoundError: + self.logger.warning(f"显示器 {monitor_name} 配置文件未找到: {config_path}") + except Exception as e: + self.logger.error(f"加载显示器 {monitor_name} 配置文件时出错: {str(e)}") + + return None \ No newline at end of file diff --git a/opt/ktouch/src/display_set b/opt/ktouch/src/display_set new file mode 100644 index 0000000..232e5d4 --- /dev/null +++ b/opt/ktouch/src/display_set @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +""" +显示器配置检查与设置脚本 +环境变量: DISPLAY=:0 +参数: 配置文件路径 +配置文件格式: 分辨率@刷新率|坐标|旋转方向|是否是主屏 +示例: 1920x1080@60.0Hz|0,0|1|P +功能: 检查当前分辨率是否与配置文件一致,否则进行设置 +""" + +import os +import sys +import re +import subprocess +import argparse +from pathlib import Path + +class DisplayChecker: + def __init__(self, display_env=":0"): + """初始化显示器检查器""" + self.display_env = display_env + self.display_name = None + self.config_data = None + self.current_settings = None + + def parse_config_file(self, config_path): + """ + 解析配置文件 + 格式: 分辨率@刷新率|坐标|旋转方向|是否是主屏 + 示例: 1920x1080@60.0Hz|0,0|1|P + """ + try: + with open(config_path, 'r') as f: + content = f.read().strip() + + # 从文件名获取显示器名称(去掉后缀) + self.display_name = Path(config_path).stem + + # 解析配置内容 + parts = content.split('|') + if len(parts) != 4: + raise ValueError("配置文件格式错误,应该有4个部分") + + # 解析分辨率和刷新率 + res_refresh_match = re.match(r'(\d+)x(\d+)@([\d.]+)Hz', parts[0]) + if not res_refresh_match: + raise ValueError("分辨率刷新率格式错误") + + width, height, refresh_rate = res_refresh_match.groups() + + # 解析坐标 + pos_match = re.match(r'(-?\d+),(-?\d+)', parts[1]) + if not pos_match: + raise ValueError("坐标格式错误") + + pos_x, pos_y = pos_match.groups() + + # 解析旋转方向 + rotation_map = {'1': 'normal', '2': 'left', '4': 'inverted', '8': 'right'} + rotation = rotation_map.get(parts[2]) + if not rotation: + raise ValueError(f"旋转方向错误: {parts[2]}") + + # 解析主屏设置 + primary = parts[3] == 'P' + + self.config_data = { + 'width': int(width), + 'height': int(height), + 'refresh_rate': float(refresh_rate), + 'pos_x': int(pos_x), + 'pos_y': int(pos_y), + 'rotation': rotation, + 'primary': primary + } + + return True + + except Exception as e: + print(f"解析配置文件错误: {e}") + return False + + def run_xrandr_command(self, cmd, ignore_errors=False): + """运行xrandr命令""" + env = os.environ.copy() + env['DISPLAY'] = self.display_env + + try: + result = subprocess.run(cmd, shell=True, env=env, capture_output=True, text=True) + if result.returncode != 0: + if ignore_errors: + # 检查是否是"已经存在"之类的错误 + error_lower = result.stderr.lower() + if any(keyword in error_lower for keyword in ['already exists', 'already set', 'exist']): + print(f"忽略预期中的错误: {result.stderr.strip()}") + return True + else: + print(f"命令执行失败但忽略错误: {cmd}") + print(f"错误信息: {result.stderr}") + return True + else: + print(f"命令执行失败: {cmd}") + print(f"错误信息: {result.stderr}") + return False + return True + except Exception as e: + print(f"执行命令错误: {e}") + return False + + def get_current_display_settings(self): + """获取当前显示器的设置""" + env = os.environ.copy() + env['DISPLAY'] = self.display_env + + try: + result = subprocess.run( + ['xrandr'], + env=env, + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("获取显示器信息失败") + return None + + # 解析xrandr输出,找到指定显示器的当前设置 + lines = result.stdout.split('\n') + current_settings = {} + + for line in lines: + # 查找目标显示器的连接状态行 + if line.startswith(f'{self.display_name} '): + # 示例: "HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 527mm x 296mm" + # 或者: "HDMI-1 connected 1920x1080+0+0" + + # 检查是否是主屏 + current_settings['primary'] = 'primary' in line + + # 提取当前分辨率 + res_match = re.search(r'(\d+x\d+)\+(-?\d+)\+(-?\d+)', line) + if res_match: + current_settings['resolution'] = res_match.group(1) + current_settings['pos_x'] = int(res_match.group(2)) + current_settings['pos_y'] = int(res_match.group(3)) + + # 提取旋转信息 + if 'inverted' in line and 'x axis y axis' not in line: + current_settings['rotation'] = 'inverted' + elif 'left' in line and 'x axis y axis' not in line: + current_settings['rotation'] = 'left' + elif 'right' in line and 'x axis y axis' not in line: + current_settings['rotation'] = 'right' + else: + current_settings['rotation'] = 'normal' + + # 继续查找当前模式行以获取刷新率 + for next_line in lines[lines.index(line)+1:]: + if next_line.strip() and not next_line.startswith(' '): + break + + # 查找当前模式(带*的) + if '*'+'+' in next_line or '* ' in next_line: + rate_match = re.search(r'(\d+\.\d+)\*', next_line) + if rate_match: + current_settings['refresh_rate'] = float(rate_match.group(1)) + break + + self.current_settings = current_settings + return current_settings + + except Exception as e: + print(f"获取当前显示器设置错误: {e}") + return None + + def get_display_modes(self): + """ + 获取显示器支持的模式列表 + 返回: 字典,键为分辨率,值为该分辨率下支持的刷新率列表 + """ + env = os.environ.copy() + env['DISPLAY'] = self.display_env + + try: + result = subprocess.run( + ['xrandr'], + env=env, + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("获取显示器信息失败") + return {} + + # 解析xrandr输出,找到指定显示器的模式 + lines = result.stdout.split('\n') + modes = {} + in_target_display = False + current_resolution = None + + for line in lines: + # 检查是否进入目标显示器的部分 + if line.startswith(f'{self.display_name} '): + in_target_display = True + continue + elif in_target_display and line.strip() and not line.startswith(' '): + # 新的显示器部分开始,退出 + break + + if in_target_display: + # 解析分辨率行,例如: "1920x1080" 或 "1280x720i" (隔行扫描) + res_match = re.search(r'^\s*(\d+x\d+i?)\s+', line) + if res_match: + current_resolution = res_match.group(1) + modes[current_resolution] = [] + + # 解析刷新率,例如: "60.00*+", "59.94", "50.00" + if current_resolution: + rate_matches = re.findall(r'(\d+\.\d+)\*?\+?', line) + for rate in rate_matches: + modes[current_resolution].append(float(rate)) + + return modes + + except Exception as e: + print(f"获取显示器模式错误: {e}") + return {} + + def get_existing_modelines(self): + """获取已经存在的自定义模式""" + env = os.environ.copy() + env['DISPLAY'] = self.display_env + + try: + result = subprocess.run( + ['xrandr'], + env=env, + capture_output=True, + text=True + ) + + if result.returncode != 0: + return [] + + # 查找自定义模式(通常在输出顶部) + lines = result.stdout.split('\n') + modelines = [] + capturing_modelines = True + + for line in lines: + # 当遇到第一个显示器输出时停止捕获 + if re.match(r'^\S+ connected', line): + capturing_modelines = False + continue + + if capturing_modelines and line.strip(): + # 模式行通常包含分辨率 + mode_match = re.match(r'^\s*(\S+)\s+.*?(\d+\.\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)', line) + if mode_match: + modelines.append(mode_match.group(1)) + + return modelines + + except Exception as e: + print(f"获取已存在模式错误: {e}") + return [] + + def find_matching_mode(self, modes): + """在支持的模式中查找匹配的模式(允许刷新率误差)""" + target_res = f"{self.config_data['width']}x{self.config_data['height']}" + target_refresh = self.config_data['refresh_rate'] + + # 检查精确匹配的分辨率 + if target_res in modes: + for refresh_rate in modes[target_res]: + # 允许±2fps的误差 + if abs(refresh_rate - target_refresh) <= 2.0: + return f"{target_res} {refresh_rate}" + + # 检查隔行扫描变体(如果有) + interlaced_res = f"{target_res}i" + if interlaced_res in modes: + for refresh_rate in modes[interlaced_res]: + # 允许±2fps的误差 + if abs(refresh_rate - target_refresh) <= 2.0: + return f"{interlaced_res} {refresh_rate}" + + return None + + def create_and_add_mode(self): + """使用cvt创建并添加新的显示模式""" + width = self.config_data['width'] + height = self.config_data['height'] + refresh_rate = self.config_data['refresh_rate'] + + # 生成模式名称 + mode_name = f"{width}x{height}_{refresh_rate}" + + # 检查模式是否已经存在 + existing_modelines = self.get_existing_modelines() + if mode_name in existing_modelines: + print(f"模式 {mode_name} 已经存在,跳过创建") + else: + # 使用cvt生成模式行 + try: + # 运行cvt命令 + result = subprocess.run( + ['cvt', str(width), str(height), str(refresh_rate)], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("cvt命令执行失败") + return None + + # 从cvt输出中提取模式行 + lines = result.stdout.split('\n') + modeline = None + for line in lines: + if line.startswith('Modeline '): + modeline = line.replace('Modeline ', '').strip() + break + + if not modeline: + print("无法从cvt输出中提取模式行") + return None + + # 添加新模式(忽略已存在的错误) + add_mode_cmd = f"xrandr --newmode {mode_name} {modeline.split(' ', 1)[1]}" + if not self.run_xrandr_command(add_mode_cmd, ignore_errors=True): + return None + print(f"成功创建新模式: {mode_name}") + + except Exception as e: + print(f"创建模式错误: {e}") + return None + + # 将新模式添加到显示器(忽略已添加的错误) + add_to_output_cmd = f"xrandr --addmode {self.display_name} {mode_name}" + if not self.run_xrandr_command(add_to_output_cmd, ignore_errors=True): + return None + + return mode_name + + def parse_resolution(self, resolution_str): + """解析分辨率字符串,返回宽高元组""" + try: + if 'x' in resolution_str: + width, height = resolution_str.split('x') + # 处理隔行扫描(如1080i) + height = height.replace('i', '') + return int(width), int(height) + return 0, 0 + except: + return 0, 0 + + def compare_settings(self): + """比较当前设置与配置设置""" + if not self.current_settings or not self.config_data: + return False, "无法获取当前设置或配置数据" + + # 检查分辨率(允许±10像素误差) + current_res = self.current_settings.get('resolution', '') + current_width, current_height = self.parse_resolution(current_res) + target_width = self.config_data['width'] + target_height = self.config_data['height'] + + width_diff = abs(current_width - target_width) + height_diff = abs(current_height - target_height) + + if width_diff > 10 or height_diff > 10: + return False, f"分辨率不匹配: 当前 {current_res} ({width_diff}, {height_diff} 像素差异), 配置 {target_width}x{target_height}" + + # 检查刷新率(允许±2fps误差) + current_refresh = self.current_settings.get('refresh_rate', 0) + target_refresh = self.config_data['refresh_rate'] + refresh_diff = abs(current_refresh - target_refresh) + + if refresh_diff > 2.0: + return False, f"刷新率不匹配: 当前 {current_refresh:.1f}Hz ({refresh_diff:.1f}Hz 差异), 配置 {target_refresh:.1f}Hz" + + # 检查旋转(必须精确匹配) + current_rotation = self.current_settings.get('rotation', 'normal') + if current_rotation != self.config_data['rotation']: + return False, f"旋转不匹配: 当前 {current_rotation}, 配置 {self.config_data['rotation']}" + + # 检查主屏设置(必须精确匹配) + current_primary = self.current_settings.get('primary', False) + if current_primary != self.config_data['primary']: + primary_status_current = "是" if current_primary else "否" + primary_status_target = "是" if self.config_data['primary'] else "否" + return False, f"主屏设置不匹配: 当前 {primary_status_current}, 配置 {primary_status_target}" + + # 位置不进行校验(根据要求) + + # 生成详细的匹配信息 + match_details = [] + if width_diff > 0 or height_diff > 0: + match_details.append(f"分辨率: {current_res} (差异: {width_diff}x{height_diff} 像素)") + else: + match_details.append(f"分辨率: {current_res} (精确匹配)") + + if refresh_diff > 0: + match_details.append(f"刷新率: {current_refresh:.1f}Hz (差异: {refresh_diff:.1f}Hz)") + else: + match_details.append(f"刷新率: {current_refresh:.1f}Hz (精确匹配)") + + match_details.append(f"旋转: {current_rotation}") + match_details.append(f"主屏: {'是' if current_primary else '否'}") + match_details.append("位置: 跳过校验") + + return True, " | ".join(match_details) + + def configure_display(self): + """配置显示器""" + if not self.config_data: + print("没有可用的配置数据") + return False + + # 获取当前支持的模式 + modes = self.get_display_modes() + if not modes: + print(f"无法获取显示器 {self.display_name} 的模式信息") + return False + + print(f"显示器 {self.display_name} 支持的模式:") + for res, rates in modes.items(): + print(f" {res}: {rates}") + + # 查找匹配的模式 + mode_name = self.find_matching_mode(modes) + + # 如果不支持,创建新模式 + if not mode_name: + print("显示器不支持该模式,尝试创建新模式...") + mode_name = self.create_and_add_mode() + if not mode_name: + print("创建新模式失败") + return False + else: + print(f"找到匹配模式: {mode_name}") + + # 构建xrandr命令 + cmd_parts = ["xrandr", f"--output {self.display_name}"] + + # 添加模式 + if ' ' in mode_name: + resolution, rate = mode_name.split(' ', 1) + cmd_parts.append(f"--mode {resolution}") + cmd_parts.append(f"--rate {rate}") + else: + # 自定义模式 + cmd_parts.append(f"--mode {mode_name}") + + # 添加位置(即使不校验也设置) + cmd_parts.append(f"--pos {self.config_data['pos_x']}x{self.config_data['pos_y']}") + + # 添加旋转 + cmd_parts.append(f"--rotate {self.config_data['rotation']}") + + # 如果是主屏 + if self.config_data['primary']: + cmd_parts.append("--primary") + + # 启用显示器 + cmd_parts.append("--auto") + + # 执行最终配置命令 + final_cmd = " ".join(cmd_parts) + print(f"执行命令: {final_cmd}") + + if self.run_xrandr_command(final_cmd): + print("显示器配置成功!") + return True + else: + print("显示器配置失败!") + return False + + def check_and_configure(self): + """检查并配置显示器""" + # 获取当前设置 + current_settings = self.get_current_display_settings() + if not current_settings: + print("无法获取当前显示器设置,尝试直接配置...") + return self.configure_display() + + print("当前显示器设置:") + for key, value in current_settings.items(): + print(f" {key}: {value}") + + # 比较设置 + match, message = self.compare_settings() + + if match: + print(f"✓ 设置匹配: {message}") + return True + else: + print(f"✗ 设置不匹配: {message}") + print("开始配置显示器...") + return self.configure_display() + +def main(): + """主函数""" + # 检查DISPLAY环境变量 + if 'DISPLAY' not in os.environ: + os.environ['DISPLAY'] = ':0' + print("设置DISPLAY环境变量为:0") + + # 解析命令行参数 + parser = argparse.ArgumentParser(description='显示器配置检查与设置脚本') + parser.add_argument('config_file', help='配置文件路径') + args = parser.parse_args() + + # 检查配置文件是否存在 + if not os.path.exists(args.config_file): + print(f"配置文件不存在: {args.config_file}") + sys.exit(1) + + # 创建检查器并执行检查与配置 + checker = DisplayChecker() + + if checker.parse_config_file(args.config_file): + if checker.check_and_configure(): + print("显示器检查与配置完成") + sys.exit(0) + else: + print("显示器检查与配置失败") + sys.exit(1) + else: + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opt/ktouch/src/gdk_test.c b/opt/ktouch/src/gdk_test.c new file mode 100644 index 0000000..e1057bb --- /dev/null +++ b/opt/ktouch/src/gdk_test.c @@ -0,0 +1,8 @@ +#include +#include + +int main(int argc, char *argv[]) { + gtk_init(&argc, &argv); + GdkMonitor *monitor; // 仅测试该类型是否可识别 + return 0; +} diff --git a/opt/ktouch/src/kdisplay.py b/opt/ktouch/src/kdisplay.py new file mode 100644 index 0000000..6b90b78 --- /dev/null +++ b/opt/ktouch/src/kdisplay.py @@ -0,0 +1,818 @@ +#!/usr/bin/env python3 +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog +import logging +import time +import os +from display_control import DisplayControl +import sys + +import sv_ttk + +def get_real_path(relative_path): + """获取资源的正确绝对路径""" + try: + # 打包后的情况 + base_path = sys._MEIPASS + except AttributeError: + # 开发环境的情况 + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + + +def get_display_info(): + # 默认临时文件路径 + temp_file = "/tmp/display_selection.txt" + + # 检查环境变量中是否有自定义路径 + if 'DISPLAY_SELECTION_FILE' in os.environ: + temp_file = os.environ['DISPLAY_SELECTION_FILE'] + + display_info = {} + + if os.path.exists(temp_file): + with open(temp_file, 'r') as f: + for line in f: + if '=' in line: + key, value = line.strip().split('=', 1) + display_info[key] = value + + # 转换为整数 + for key in ['MONITOR', 'X', 'Y', 'WIDTH', 'HEIGHT']: + if key in display_info: + try: + display_info[key] = int(display_info[key]) + except ValueError: + # 如果转换失败,使用默认值 + if key == 'MONITOR': + display_info[key] = 0 + elif key in ['X', 'Y']: + display_info[key] = 0 + elif key == 'WIDTH': + display_info[key] = 1920 + elif key == 'HEIGHT': + display_info[key] = 1080 + + return display_info + +class DisplaySettingsGUI: + def __init__(self, root): + self.root = root + self.root.title("显示器设置工具(for UOS)") + # """根据/tmp/display_selection.txt设置窗口位置""" + display_info = get_display_info() + target_x = 100 + target_x = 100 + window_width = 950 # 增加窗口宽度 + window_height = 600 # 增加窗口高度 + + try: + # 读取配置 + x = display_info.get('X', 0) + y = display_info.get('Y', 0) + width = display_info.get('WIDTH', 1280) + height = display_info.get('HEIGHT', 720) + + target_x = int( x + (width - window_width) // 2) + target_y = int(y + (height - window_height) // 2) + except: + pass + + self.root.geometry(f"{window_width}x{window_height}+{target_x}+{target_y}") + self.root.resizable(True, True) + + + # 显示器控制对象 + self.display_control = DisplayControl() + + # 显示器信息 + self.monitors = [] + self.monitor_vars = [] + self.resolution_vars = [] + self.rotation_vars = [] + self.primary_vars = [] + + # 旋转方向映射 + self.rotation_names = { + 1: "正常", + 2: "向左90度", + 4: "翻转", + 8: "向右90度" + } + + # 创建界面 + self.create_widgets() + + # 获取显示器信息 + self.refresh_monitor_info() + + + def create_widgets(self): + # 主框架 + main_frame = ttk.Frame(self.root, padding="10") # 增加内边距 + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 配置网格权重 + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(1, weight=1) + + # 标题 + # title_label = ttk.Label(main_frame, text="显示器设置工具(for UOS)", font=("Arial", 12, "bold")) + # title_label.grid(row=0, column=0, columnspan=2, pady=(0, 15)) # 增加下边距 + + # 左侧设置面板 + settings_frame = ttk.LabelFrame(main_frame, text="显示器设置", padding="10") # 增加内边距 + settings_frame.grid(row=1, column=0,columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 15)) # 增加右边距 + + # 右侧示意图面板 + diagram_frame = ttk.LabelFrame(main_frame, text="显示器排列示意图", padding="10") # 增加内边距 + diagram_frame.grid(row=1, column=2, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 配置网格权重 + main_frame.rowconfigure(1, weight=1) + main_frame.columnconfigure(1, weight=1) + settings_frame.columnconfigure(0, weight=2) + settings_frame.rowconfigure(0, weight=1) + diagram_frame.columnconfigure(0, weight=1) + diagram_frame.rowconfigure(0, weight=1) + + # 画布用于显示示意图 + self.canvas = tk.Canvas(diagram_frame, bg="#f0f0f0", relief=tk.SUNKEN, bd=1) + self.canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 创建设置区域的滚动框架 + self.scrollable_frame = ttk.Frame(settings_frame) + self.scrollable_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 创建滚动条 + scrollbar = ttk.Scrollbar(settings_frame, orient="vertical") + scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # 创建画布用于滚动 + self.settings_canvas = tk.Canvas(self.scrollable_frame, yscrollcommand=scrollbar.set) + self.settings_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 配置滚动条 + scrollbar.config(command=self.settings_canvas.yview) + + # 创建设置区域的内部框架 + self.settings_inner_frame = ttk.Frame(self.settings_canvas, padding="1") # 增加内边距 + self.settings_canvas.create_window((0, 0), window=self.settings_inner_frame, anchor="nw") + + # 绑定配置事件以更新滚动区域 + self.settings_inner_frame.bind("", self.on_settings_frame_configure) + + # 按钮框架 + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=2, column=0, columnspan=2, pady=(15, 0)) # 增加上边距 + + # 刷新按钮 + refresh_btn = ttk.Button(button_frame, text="刷新显示器信息", command=self.refresh_monitor_info) + refresh_btn.pack(side=tk.LEFT, padx=(0, 10)) + + # 应用并保存按钮 + apply_save_btn = ttk.Button(button_frame, text="应用并保存设置", command=self.apply_and_save_settings) + apply_save_btn.pack(side=tk.LEFT) + + def on_settings_frame_configure(self, event): + """更新滚动区域""" + self.settings_canvas.configure(scrollregion=self.settings_canvas.bbox("all")) + + def refresh_monitor_info(self): + """获取显示器信息并更新界面""" + try: + self.monitors = self.display_control.get_monitors() + self.update_settings_ui() + self.update_diagram() + except Exception as e: + messagebox.showerror("错误", f"获取显示器信息时出错: {str(e)}") + + def update_settings_ui(self): + """更新设置界面""" + # 清除现有控件 + for widget in self.settings_inner_frame.winfo_children(): + widget.destroy() + + self.monitor_vars = [] + self.resolution_vars = [] + self.rotation_vars = [] + self.primary_vars = [] + + # 获取所有可用的位置选项 + position_options = [str(i+1) for i in range(len(self.monitors))] + + # 为每个显示器创建设置控件 + for i, monitor in enumerate(self.monitors): + monitor_frame = ttk.LabelFrame(self.settings_inner_frame, text=monitor['name']) + monitor_frame.grid(row=i, column=0, sticky=(tk.W, tk.E), padx=(0,10),pady=(0, 15), ipadx=5, ipady=3) # 增加下边距 + monitor_frame.columnconfigure(1, weight=1) + + # 分辨率设置(放在第一行) + ttk.Label(monitor_frame, text="分辨率:").grid(row=0, column=0, sticky=tk.W, padx=(0, 20)) # 增加右边距 + res_var = tk.StringVar() + resolutions = [mode['label'] for mode in monitor['modes']] + # print(resolutions) + + # 设置分辨率 + current_res_index = 0 + if monitor["current_mode"][0] == 0: + # 自定义分辨率 + resolutions = [f"自定义:{monitor['current_mode'][1]}x{monitor['current_mode'][2]}@{monitor['current_mode'][3]}Hz"] + resolutions + else: + for idx, mode in enumerate(monitor['modes']): + if mode['id'] == monitor['current_mode'][0]: + current_res_index = idx + break + + res_combo = ttk.Combobox(monitor_frame, textvariable=res_var, values=resolutions, state="readonly", width=25,height=20) # 增加宽度 + res_combo.current(current_res_index) + res_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5) + + # 绑定分辨率变化事件 + res_combo.bind('<>', + lambda e, m=monitor, v=res_var: self.on_resolution_change(m, v)) + + self.resolution_vars.append(res_var) + + # 第二行:位置、旋转和主显示器设置 + options_frame = ttk.Frame(monitor_frame) + options_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) + + # 位置设置 + ttk.Label(options_frame, text="位置:").pack(side=tk.LEFT, padx=(0, 5)) + pos_var = tk.StringVar(value=str(monitor['position'])) + pos_combo = ttk.Combobox(options_frame, textvariable=pos_var, values=position_options, + state="readonly", width=5) + pos_combo.pack(side=tk.LEFT, padx=(0, 10)) + pos_combo.bind('<>', + lambda e, m=monitor, v=pos_var: self.update_monitor_position(m, v)) + self.monitor_vars.append(pos_var) + + # 分隔线 + ttk.Separator(options_frame, orient='vertical').pack(side=tk.LEFT, padx=10, fill=tk.Y) + + # 旋转设置 + ttk.Label(options_frame, text="旋转:").pack(side=tk.LEFT, padx=(0, 5)) + rotation_var = tk.StringVar(value=str(monitor['rotation'])) + rotation_combo = ttk.Combobox(options_frame, textvariable=rotation_var, + values=["正常", "向左90度", "翻转", "向右90度"], state="readonly", width=10) + rotation_combo.pack(side=tk.LEFT, padx=(0, 10)) + + # 设置显示文本为旋转方向名称 + rotation_name = self.rotation_names.get(monitor['rotation'], "正常") + rotation_combo.set(rotation_name) + + # 绑定旋转变化事件 + rotation_combo.bind('<>', + lambda e, m=monitor, v=rotation_var: self.on_rotation_change(m, v)) + + self.rotation_vars.append(rotation_var) + + # 分隔线 + ttk.Separator(options_frame, orient='vertical').pack(side=tk.LEFT, padx=10, fill=tk.Y) + + # 主屏设置 + primary_var = tk.BooleanVar(value=monitor['is_primary']) + primary_check = ttk.Checkbutton(options_frame, text="主显示器", + variable=primary_var, + command=lambda v=primary_var, m=monitor: self.update_primary_monitor(v, m)) + primary_check.pack(side=tk.LEFT) + + self.primary_vars.append(primary_var) + + def on_resolution_change(self, monitor, var): + """当分辨率改变时更新显示器信息""" + selected_res = var.get() + + if selected_res == "自定义分辨率": + self.prompt_custom_resolution(monitor, var) + else: + # 解析分辨率并更新显示器信息 + if "自定义" in selected_res: + selected_res = selected_res[4:] + parts = selected_res.split(' @ ') + resolution = parts[0].split('x') + width = int(resolution[0]) + height = int(resolution[1]) + + monitor['width'] = width + monitor['height'] = height + + # 更新所有显示器的坐标 + self.update_all_monitor_positions() + + # 更新示意图 + self.update_diagram() + + def update_all_monitor_positions(self): + """更新所有显示器的坐标""" + # 按位置分组显示器 + position_groups = {} + for monitor in self.monitors: + pos = monitor['position'] + if pos not in position_groups: + position_groups[pos] = [] + position_groups[pos].append(monitor) + + # 计算每个位置组的X坐标 + x_positions = {} + x_offset = 0 + for pos in sorted(position_groups.keys()): + # 使用组中第一个显示器的宽度 + monitor = position_groups[pos][0] + x_positions[pos] = x_offset + x_offset += monitor['width'] + + # 更新所有显示器的坐标 + for monitor in self.monitors: + position = monitor['position'] + monitor['x'] = x_positions.get(position, 0) + monitor['y'] = 0 # 假设所有显示器在垂直方向对齐 + + def prompt_custom_resolution(self, monitor, res_var): + """提示用户输入自定义分辨率""" + dialog = CustomResolutionDialog(self.root, monitor) + result = dialog.result + + if result: + width, height, refresh_rate, force_custom = result + monitor['custom_width'] = width + monitor['custom_height'] = height + monitor['custom_refresh_rate'] = refresh_rate + monitor['force_custom'] = force_custom + + # 更新显示器信息 + monitor['width'] = width + monitor['height'] = height + + # 更新所有显示器的坐标 + self.update_all_monitor_positions() + + # 更新下拉框显示 + + res_var.set(f"自定义:{width}x{height}@{refresh_rate}Hz") + + # 更新示意图 + self.update_diagram() + + def on_rotation_change(self, monitor, var): + """当旋转方向改变时更新显示器信息""" + rotation_str = var.get() + rotation_map = { + "正常": 1, + "向左90度": 2, + "翻转": 4, + "向右90度": 8 + } + rotation = rotation_map.get(rotation_str, 1) + old_rotation = monitor["rotation"] + + if old_rotation in [1, 4]: + if rotation in [2,8]: + buff = monitor['width'] + monitor['width'] = monitor['height'] + monitor['height'] = buff + else: + if rotation in [1,4]: + buff = monitor['width'] + monitor['width'] = monitor['height'] + monitor['height'] = buff + + + monitor['rotation'] = rotation + + self.update_all_monitor_positions() + + # 更新示意图 + self.update_diagram() + + def update_monitor_position(self, monitor, var): + """更新显示器位置""" + try: + position = int(var.get()) + if 1 <= position <= len(self.monitors): + old_position = monitor.get('position', 1) + monitor['position'] = position + + # 更新所有显示器的坐标 + self.update_all_monitor_positions() + + self.update_diagram() + except ValueError: + pass + + def update_primary_monitor(self, var, monitor): + """更新主显示器设置""" + if var.get(): + # 取消其他显示器的选中状态 + for i, primary_var in enumerate(self.primary_vars): + if primary_var != var: + primary_var.set(False) + for m in self.monitors: + m["is_primary"] = False + + # 设置主显示器 + monitor['is_primary'] = True + + # 更新示意图 + self.update_diagram() + else: + # 不允许取消主显示器,至少需要有一个主显示器 + var.set(True) + # messagebox.showwarning("警告", "至少需要有一个主显示器") + + def update_diagram(self): + """更新显示器排列示意图,根据实际坐标进行渲染""" + + + if not self.monitors: + return + + self.canvas.delete("all") + + # 计算所有显示器的边界 + min_x = min(monitor.get('x', 0) for monitor in self.monitors) + max_x = max(monitor.get('x', 0) + monitor['width'] for monitor in self.monitors) + min_y = min(monitor.get('y', 0) for monitor in self.monitors) + max_y = max(monitor.get('y', 0) + monitor['height'] for monitor in self.monitors) + + # 计算画布大小和缩放比例 + canvas_width = self.canvas.winfo_width() + canvas_height = self.canvas.winfo_height() + + if canvas_width <= 1 or canvas_height <= 1: + # 画布尚未渲染,使用默认大小 + canvas_width = 400 + canvas_height = 300 + + # 计算总宽度和高度 + total_width = max_x - min_x + total_height = max_y - min_y + + # 计算缩放比例,保留边距 + scale_x = 0.8 * canvas_width / total_width if total_width > 0 else 1 + scale_y = 0.8 * canvas_height / total_height if total_height > 0 else 1 + scale = min(scale_x, scale_y) + + # 计算偏移量,使所有显示器居中显示 + offset_x = (canvas_width - total_width * scale) / 2 - min_x * scale + offset_y = (canvas_height - total_height * scale) / 2 - min_y * scale + + # 按坐标和分辨率分组显示器 + coord_groups = {} + for monitor in self.monitors: + x = monitor.get('x', 0) + y = monitor.get('y', 0) + width = monitor['width'] + height = monitor['height'] + + # 创建分组键 + group_key = (x, y, width, height) + + if group_key not in coord_groups: + coord_groups[group_key] = [] + coord_groups[group_key].append(monitor) + + # 绘制每个组的显示器 + for group_key, monitors in coord_groups.items(): + x, y, width, height = group_key + + # 计算在画布上的位置 + canvas_x = offset_x + x * scale + canvas_y = offset_y + y * scale + canvas_width_scaled = width * scale + canvas_height_scaled = height * scale + + # 绘制组的背景矩形(仅在多个显示器共享相同坐标和分辨率时) + if len(monitors) > 1: + self.canvas.create_rectangle( + canvas_x, canvas_y, + canvas_x + canvas_width_scaled, canvas_y + canvas_height_scaled, + fill="#e0e0e0", outline="#a0a0a0", width=1, dash=(5, 5) + ) + + # 收集所有显示器的名称和旋转信息 + all_names = [] + for monitor in monitors: + rotation_text = self.rotation_names.get(monitor['rotation'], "未知") + name_text = f"{monitor['name']} " + if monitor['is_primary']: + name_text += " [主]" + all_names.append(name_text) + + # 在组上方显示所有显示器名称 + if all_names: + name_text = "\n".join(all_names) + self.canvas.create_text( + canvas_x + canvas_width_scaled / 2, canvas_y - 20, + text=name_text, + font=("Arial", 9, "bold"), + justify=tk.CENTER, + fill="blue" + ) + + # 绘制每个显示器(按面积从大到小排序,小的覆盖大的) + sorted_monitors = sorted(monitors, key=lambda m: m['width'] * m['height'], reverse=True) + + # 计算重叠显示器的偏移量 + offset_step = min(10 * scale, 10) # 最大偏移10像素 + max_offset = offset_step * (len(sorted_monitors) - 1) + + for i, monitor in enumerate(sorted_monitors): + # 计算显示器的偏移位置 + monitor_x = canvas_x + min(i * offset_step, max_offset) + monitor_y = canvas_y + min(i * offset_step, max_offset) + + # 绘制矩形 + fill_color = "lightblue" if monitor['is_primary'] else "white" + rect_id = self.canvas.create_rectangle( + monitor_x, monitor_y, + monitor_x + canvas_width_scaled, monitor_y + canvas_height_scaled, + fill=fill_color, outline="black", width=2 + ) + + # 添加显示器分辨率和旋转信息 + rotation_text = self.rotation_names.get(monitor['rotation'], "未知") + text_lines = [ + f"{monitor['width']}x{monitor['height']}", + f"旋转: {rotation_text}" + ] + text_content = "\n".join(text_lines) + + text_id = self.canvas.create_text( + monitor_x + canvas_width_scaled/2, monitor_y + canvas_height_scaled/2, + text=text_content, + font=("Arial", 8), + justify=tk.CENTER + ) + + # 添加坐标标签 + coord_text = f"({x}, {y})" + self.canvas.create_text( + canvas_x + canvas_width_scaled / 2, canvas_y + canvas_height_scaled + 15, + text=coord_text, + font=("Arial", 8), + fill="green" + ) + + def apply_settings(self): + """应用显示器设置""" + try: + # 检查位置冲突 + position_groups = {} + for monitor in self.monitors: + pos = monitor['position'] + if pos not in position_groups: + position_groups[pos] = [] + position_groups[pos].append(monitor) + + # 检查是否有重叠的显示器 + has_overlap = any(len(monitors) > 1 for monitors in position_groups.values()) + + if has_overlap: + overlap_info = [] + for pos, monitors in position_groups.items(): + if len(monitors) > 1: + monitor_names = ", ".join([m['name'] for m in monitors]) + overlap_info.append(f"位置 {pos}: {monitor_names}") + + warning_msg = "以下位置的显示器将重叠显示:\n" + "\n".join(overlap_info) + if not messagebox.askyesno("位置重叠警告", f"{warning_msg}\n\n是否继续?"): + return + + # 计算每个位置组的X坐标 + x_positions = {} + x_offset = 0 + for pos in sorted(position_groups.keys()): + # 使用组中第一个显示器的宽度 + monitor = position_groups[pos][0] + x_positions[pos] = x_offset + x_offset += monitor['width'] + + # 在设置主显示器之前调用SwitchMode + self.display_control.switch_mode() + + # 为每个显示器应用设置 + for i, monitor in enumerate(self.monitors): + # 设置分辨率 + selected_res = self.resolution_vars[i].get() + # print(selected_res) + if selected_res: + + if "自定义" in selected_res: + config="" + # 使用自定义分辨率 + # 设置旋转 + rotation_str = self.rotation_vars[i].get() + rotation_map = { + "正常": 1, + "向左90度": 2, + "翻转": 4, + "向右90度": 8 + } + rotation = rotation_map.get(rotation_str, 1) + + # 设置位置 + position = monitor['position'] + x = x_positions.get(position, 0) + y = 0 + + # 设置主显示器 + isPrimary="N" + if self.primary_vars[i].get(): + isPrimary="P" + + # 添加强制标志 + force_flag = "F" if monitor.get('force_custom', False) else "A" + + config=f'{selected_res[4:]}|{x},{y}|{rotation}|{isPrimary}|{force_flag}' + # print(config) + self.display_control.save_custom_resolution(monitor['name'], config) + continue + else: + self.display_control.remove_custom_resolution(monitor['name']) + # 解析分辨率和刷新率 + parts = selected_res.split(' @ ') + resolution = parts[0].split('x') + width = int(resolution[0]) + height = int(resolution[1]) + refresh_rate = float(parts[1].replace('Hz', '')) + + # 优先使用Mode接口 + mode_id = None + for mode in monitor['modes']: + if mode['label'] == selected_res: + mode_id = mode['id'] + break + + if mode_id and mode_id != 'custom': + self.display_control.set_mode(monitor['path'], mode_id) + else: + # 备用方案:使用SetModeBySize和SetRefreshRate接口 + self.display_control.set_mode_by_size(monitor['path'], width, height) + self.display_control.set_refresh_rate(monitor['path'], refresh_rate) + + # 设置旋转 + rotation_str = self.rotation_vars[i].get() + rotation_map = { + "正常": 1, + "向左90度": 2, + "翻转": 4, + "向右90度": 8 + } + rotation = rotation_map.get(rotation_str, 1) + self.display_control.set_rotation(monitor['path'], rotation) + + # 设置位置 + position = monitor['position'] + x = x_positions.get(position, 0) + y = 0 # 假设所有显示器在垂直方向对齐 + self.display_control.set_position(monitor['path'], x, y) + + # 设置主显示器 + if self.primary_vars[i].get(): + self.display_control.set_primary(monitor['name']) + + # 应用更改 + self.display_control.apply_changes() + + # 等待1秒后刷新显示器信息 + self.root.after(1000, self.refresh_monitor_info) + + # messagebox.showinfo("成功", "显示器设置已应用") + + except Exception as e: + messagebox.showerror("错误", f"应用设置时出错: {str(e)}") + + def apply_and_save_settings(self): + """应用并保存设置""" + try: + # 先应用设置 + self.apply_settings() + + # 3秒后保存设置 + self.root.after(3000, self.save_settings_with_delay) + + except Exception as e: + messagebox.showerror("错误", f"应用设置时出错: {str(e)}") + + def save_settings_with_delay(self): + """延迟保存设置""" + try: + file_path = "/tmp/ktouch/display_update.txt" + dir_path = "/tmp/ktouch" + # 检查目录是否存在 + if os.path.exists(dir_path): + # 写入文件(不存在则创建) + with open(file_path, 'w') as f: + f.write("1") + + self.display_control.save_settings() + + # messagebox.showinfo("成功", "显示器设置已保存") + except Exception as e: + messagebox.showerror("错误", f"保存设置时出错: {str(e)}") + + def on_resize(self, event): + """处理窗口大小变化事件""" + if event.widget == self.root: + self.update_diagram() + + +class CustomResolutionDialog: + """自定义分辨率对话框""" + def __init__(self, parent, monitor): + self.parent = parent + self.monitor = monitor + self.result = None + + self.dialog = tk.Toplevel(parent) + self.dialog.title("自定义分辨率") + self.dialog.geometry("350x250") # 增加高度以容纳新控件 + self.dialog.transient(parent) + self.dialog.grab_set() + + # 加载之前保存的自定义分辨率 + custom_width = monitor.get('custom_width', 1920) + custom_height = monitor.get('custom_height', 1080) + custom_refresh = monitor.get('custom_refresh_rate', 60.0) + force_custom = monitor.get('force_custom', False) + + # 宽度设置 + ttk.Label(self.dialog, text="宽度:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) + self.width_var = tk.StringVar(value=str(custom_width)) + ttk.Entry(self.dialog, textvariable=self.width_var).grid(row=0, column=1, padx=5, pady=5, sticky=(tk.W, tk.E)) + + # 高度设置 + ttk.Label(self.dialog, text="高度:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) + self.height_var = tk.StringVar(value=str(custom_height)) + ttk.Entry(self.dialog, textvariable=self.height_var).grid(row=1, column=1, padx=5, pady=5, sticky=(tk.W, tk.E)) + + # 刷新率设置 + ttk.Label(self.dialog, text="刷新率 (Hz):").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W) + self.refresh_var = tk.StringVar(value=str(custom_refresh)) + ttk.Entry(self.dialog, textvariable=self.refresh_var).grid(row=2, column=1, padx=5, pady=5, sticky=(tk.W, tk.E)) + + # 强制使用自定义分辨率复选框 + self.force_var = tk.BooleanVar(value=force_custom) + force_check = ttk.Checkbutton( + self.dialog, + text="强制使用自定义分辨率而不是选取驱动分辨率", + variable=self.force_var + ) + force_check.grid(row=3, column=0, columnspan=2, padx=5, pady=10, sticky=tk.W) + + # 按钮框架 + button_frame = ttk.Frame(self.dialog) + button_frame.grid(row=4, column=0, columnspan=2, pady=10) + + ttk.Button(button_frame, text="确定", command=self.on_ok).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="取消", command=self.on_cancel).pack(side=tk.LEFT, padx=5) + + # 配置网格权重 + self.dialog.columnconfigure(1, weight=1) + + self.dialog.wait_window() + + def on_ok(self): + """确定按钮处理""" + try: + width = int(self.width_var.get()) + height = int(self.height_var.get()) + refresh_rate = float(self.refresh_var.get()) + force_custom = self.force_var.get() + + if width <= 0 or height <= 0 or refresh_rate <= 0: + raise ValueError("值必须为正数") + + self.result = (width, height, refresh_rate, force_custom) + self.dialog.destroy() + + except ValueError as e: + messagebox.showerror("错误", f"请输入有效的数值: {str(e)}") + + def on_cancel(self): + """取消按钮处理""" + self.dialog.destroy() + + +def main(): + root = tk.Tk() + + # Import the tcl file + root.tk.call('source', '/opt/ktouch/forest-light.tcl') + + # Set the theme with the theme_use method + ttk.Style().theme_use('forest-light') + + app = DisplaySettingsGUI(root) + root.bind('', app.on_resize) + + # sv_ttk.use_light_theme() + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opt/ktouch/src/kscreen_remap_daemon.py37 b/opt/ktouch/src/kscreen_remap_daemon.py37 new file mode 100644 index 0000000..7dd8bcf --- /dev/null +++ b/opt/ktouch/src/kscreen_remap_daemon.py37 @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import time +import logging +import signal +import hashlib +import subprocess +from pathlib import Path +from threading import Thread +from time import sleep + +# 配置X11环境变量 +os.environ['DISPLAY'] = ':0' +# os.environ['XAUTHORITY'] = '/home/pi/.Xauthority' # 根据实际情况调整路径 + +# 全局变量 +processes = {} # 存储进程对象 +monitor_thread = None # 监控线程 +running = True # 控制循环运行 + + +# 配置日志 +def setup_logging(): + log_file = 'touchscreen.log' + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] + ) + return logging.getLogger(__name__) + +logger = setup_logging() + +# 信号处理函数 +def signal_handler(signum, frame): + global running + logger.info(f"Received signal {signum}, shutting down...") + running = False + cleanup() + +def cleanup(): + """清理函数,停止所有子进程""" + logger.info("清理子进程") + for proc_name, proc_info in processes.items(): + try: + proc = proc_info['process'] + if proc and proc.poll() is None: # 进程还在运行 + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + logger.info(f"关闭了进程: {proc_name}") + except Exception as e: + logger.error(f"关闭进程失败 {proc_name}: {e}") + processes.clear() + + +# 1. 日志文件轮转函数 +def rotate_logs(): + log_files = ['screen_calibration.log', 'touchscreen.log'] + max_size = 10 * 1024 * 1024 # 10MB + max_backups = 7 + + for log_file in log_files: + if os.path.exists(log_file) and os.path.getsize(log_file) > max_size: + # 删除最旧的备份文件 + oldest_backup = f"{log_file}.{max_backups}" + if os.path.exists(oldest_backup): + os.remove(oldest_backup) + logger.info(f"清理过早的日志: {oldest_backup}") + + # 重命名现有备份文件 + for i in range(max_backups-1, 0, -1): + old_name = f"{log_file}.{i}" + new_name = f"{log_file}.{i+1}" + if os.path.exists(old_name): + os.rename(old_name, new_name) + # logger.info(f"Renamed {old_name} to {new_name}") + + # 重命名当前日志文件 + os.rename(log_file, f"{log_file}.1") + # logger.info(f"Rotated log file: {log_file} -> {log_file}.1") + +# 2. 检查并创建所需文件 +def initialize_files(): + ktouch_dir = Path("/tmp/ktouch") + files_to_create = [ + "screen.txt", + "touch.txt", + "touch_need_update.txt", + "usbadd.txt" + ] + + # 创建目录 + ktouch_dir.mkdir(exist_ok=True) + logger.info("创建状态缓存目录 /tmp/ktouch ") + + # 创建或初始化文件 + for file_name in files_to_create: + file_path = ktouch_dir / file_name + with open(file_path, 'w') as f: + f.write('1') + logger.info(f"初始化缓存文件 {file_path}: to 1") + + +# 启动单个后台程序 +def start_process(proc_name, proc_args): + """启动单个进程并添加到监控列表""" + # 设置X11环境 + env = os.environ.copy() + env['DISPLAY'] = ':0' + + try: + proc = subprocess.Popen(proc_args, env=env) + processes[proc_name] = { + 'process': proc, + 'args': proc_args, + 'start_time': time.time(), + 'restart_count': 0 + } + logger.info(f"拉起子进程 {proc_name} (PID: {proc.pid})") + return True + except Exception as e: + logger.error(f"进程启动失败 {proc_name}: {e}") + return False + + +# 3. 启动后台程序(添加X11环境支持) +def start_background_processes(): + process_configs = { + 'screen_ds': ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"], + 'touch_ds': ["/opt/ktouch/touch_ds", "/tmp/ktouch/touchmap.txt", "/tmp/ktouch/touch_need_update.txt"], + 'usb_ds': ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"] + } + + for proc_name, proc_args in process_configs.items(): + start_process(proc_name, proc_args) + + # # 设置X11环境 + # env = os.environ.copy() + # env['DISPLAY'] = ':0' + + # for proc_args in processes: + # try: + # subprocess.Popen(proc_args, env=env) + # logger.info(f"Started process: {' '.join(proc_args)}") + # except Exception as e: + # logger.error(f"Failed to start process {proc_args[0]}: {e}") + +# 监控和重启进程 +def monitor_processes(): + """监控进程状态,如果进程退出则重启""" + global running + + while running: + try: + for proc_name, proc_info in list(processes.items()): + proc = proc_info['process'] + returncode = proc.poll() + + if returncode is not None: # 进程已退出 + logger.warning(f"进程 {proc_name} (PID: {proc.pid}) 已退出,代码 {returncode}") + + # 限制重启次数,避免无限重启 + proc_info['restart_count'] += 1 + if proc_info['restart_count'] > 128: # 最多重启128次 + logger.error(f"启动 {proc_name} 进程被结束超过128次,决定不再重启。请检查日志文件。") + del processes[proc_name] + continue + + # 等待一段时间再重启 + time.sleep(2) + + # 重新启动进程 + if start_process(proc_name, proc_info['args']): + logger.info(f"重启进程: {proc_name}") + else: + logger.error(f"重启进程失败: {proc_name}") + + # 检查是否有进程需要启动(初始启动失败的情况) + expected_processes = ['screen_ds', 'touch_ds', 'usb_ds'] + for proc_name in expected_processes: + if proc_name not in processes: + logger.warning(f"发现 {proc_name} 消失, 准备重新启动") + # 这里需要根据进程名重新构建参数 + if proc_name == 'screen_ds': + start_process(proc_name, ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"]) + elif proc_name == 'touch_ds': + start_process(proc_name, ["/opt/ktouch/touch_ds", "/tmp/ktouch/touchmap.txt", "/tmp/ktouch/touch_need_update.txt"]) + elif proc_name == 'usb_ds': + start_process(proc_name, ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"]) + + time.sleep(5) # 每2秒检查一次进程状态 + + except Exception as e: + logger.error(f"进程监控出现问题: {e}") + time.sleep(5) + + +# 4. 执行touch_remap并重置文件(添加X11环境支持) +def run_touch_remap(): + # 设置X11环境 + env = os.environ.copy() + env['DISPLAY'] = ':0' + + try: + # 执行touch_remap程序 + result = subprocess.run(["/opt/ktouch/touch_set", "/tmp/ktouch/touch.txt"], capture_output=True, text=True, env=env) + if result.returncode == 0: + logger.info("执行映射(touch_set)成功") + else: + logger.error(f"执行映射(touch_set)失败,错误信息为 {result.returncode}: {result.stderr}") + except Exception as e: + logger.error(f"无法执行touch_set映射程序!!!!!: {e}") + + # 重置文件为0 + ktouch_dir = Path("/tmp/ktouch") + files_to_reset = [ + "touch_need_update.txt", + "usbadd.txt" + ] + + for file_name in files_to_reset: + file_path = ktouch_dir / file_name + try: + with open(file_path, 'w') as f: + f.write('0') + # logger.info(f"Reset {file_path} to 0") + except Exception as e: + # logger.error(f"Failed to reset {file_path}: {e}") + +# 5. 监控文件变化 +def monitor_files(): + # 记录screen.txt的初始状态 + screen_file = "/tmp/ktouch/screen.txt" + last_screen_content = "" + + if os.path.exists(screen_file): + with open(screen_file, 'r') as f: + last_screen_content = f.read().strip() + + logger.info("开始主程序监控") + + while running: + # 检查是否需要执行touch_remap + need_update = False + + # 检查usbadd.txt和touch_need_update.txt + for file_name in ["/tmp/ktouch/usbadd.txt", "/tmp/ktouch/touch_need_update.txt"]: + try: + with open(file_name, 'r') as f: + content = f.read().strip() + if content == '1': + logger.info(f"文件 {file_name} 内容变为1,触发重映射") + need_update = True + except Exception as e: + logger.error(f"读取文件失败 {file_name}: {e}") + + # 检查screen.txt是否发生变化 + try: + with open(screen_file, 'r') as f: + current_content = f.read().strip() + if current_content != last_screen_content: + logger.info("文件 screen.txt 内容变化,触发重映射") + need_update = True + last_screen_content = current_content + except Exception as e: + logger.error(f"读取文件失败 {screen_file}: {e}") + + # 如果需要更新,执行touch_remap + if need_update: + run_touch_remap() + + # 等待一段时间再次检查 + time.sleep(1) + +# 检查X11环境是否可用 +def check_x11_environment(): + try: + # 尝试运行一个简单的X11命令来检查环境 + result = subprocess.run(['xset', '-q'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + logger.info("X11 environment is available") + return True + else: + logger.warning("X11 environment check failed") + return False + except Exception as e: + logger.error(f"Error checking X11 environment: {e}") + return False + +# 主函数 +def main(): + global monitor_thread, running + try: + # 注册信号处理 + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + logger.info("kscreen_remap_daemon 程序启动") + + # 检查X11环境 + if not check_x11_environment(): + logger.warning("主程序:X11 environment may not be available, 程序将继续运行但无效果") + + # 1. 日志轮转 + rotate_logs() + logger.info("主程序:处理日志 OK") + + # 2. 初始化文件 + initialize_files() + logger.info("主程序:初始化和检查文件 OK") + + # 3. 启动后台进程 + start_background_processes() + logger.info("主程序:启动子进程 OK") + + # 启动进程监控线程 + monitor_thread = Thread(target=monitor_processes, daemon=True) + monitor_thread.start() + logger.info("主程序:启动进程监控 OK") + + sleep(3) + # 4. 首次执行touch_remap + run_touch_remap() + logger.info("主程序:执行一次映射 OK") + + sleep(7) + # 5. 开始监控文件变化 + logger.info("主程序:开始监控loop") + monitor_files() + + + except Exception as e: + logger.error(f"Unexpected error in main: {e}") + running = False + cleanup() + sys.exit(1) + finally: + cleanup() + logger.info("Daemon script stopped") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opt/ktouch/src/readme.txt b/opt/ktouch/src/readme.txt new file mode 100644 index 0000000..6de92cd --- /dev/null +++ b/opt/ktouch/src/readme.txt @@ -0,0 +1,99 @@ +# 多个子功能源代码 + +## 源代码对应程序的功能 + +### screen_binder + touch_listen.c +创建全屏窗口给与提示,并检测触摸点击事件,生成显示器和触摸屏对应列表到当前目录下。 +生成的路径是'touch_dis_table.txt',文件格式为 显示器名|触摸屏名|触摸屏ID|触摸屏设备路径 + + +### screen_ds +检测显示器分辨率、数量、坐标是否发生了变化,需要通过参数指定记录文件路径,一般在/tmp下。 +文件格式 显示器名|分辨率XxY|坐标x,y +----- +定义路径: /tmp/ktouch/screen.txt + + +### touch_ds +检测触摸屏的矩阵是否变化,需要通过参数指定记录文件 +记格式为 12|ELAN Touchscreen|/dev/input/event5|04f3:2c2c|1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000 +touch_ds传入两个参数,第一个是对比文件的路径,第二个是通知上位程序更新的文件。 +--- +定义路径: /tmp/ktouch/touchmap.txt,/tmp/ktouch/touch_need_update.txt + +### usb_ds +检测usb是否有新的接入行为,判断接入是是否是触摸屏,需要通过参数指定记录文件。 +接入触摸屏后,将会将指定文件内容改成1,提醒主程序,主程序处理完成后应该将其改回0。 +---- +定义路径: /tmp/ktouch/usbadd.txt + +## 打包后目录中文件说明 + +check_save 设置后的二次确认窗口,以确定是否要替换触摸屏配置文件 +desktops/ 快捷方式存放的目录 +display_monitor.py 监控屏幕分辨率是否被修改的脚本,后台保持运行 +forest-light/ 显示器设置GUI的主题文件 +forest-light.tcl 显示器设置GUI的主题文件 +kdisplay 显示器设置程序 +kscreen-debug 触摸屏debug程序,用于现场快速排错 +kscreen-fix-daemon 修复service的专用脚本 +kscreen-log 采集程序日志和系统日志并打包 +kscreen-remap 单次映射/校准触摸屏 +kscreen-remap-daemon 主要功能实现的程序,在后台监控触摸屏状态及其他状态,适时操作校准 +kscreen-setup 设置触摸屏脚本 +screen_binder 设置触摸屏的GUI主体程序 +screen_ds 显示屏变化监控 +screen_ds_once 更新一次显示器信息,显示屏监控的简化版 +src 所有二进制程序的源文件,保留在程序内防止丢失 +touch_ds 对每个触摸屏的矩阵监控 +touch_set 计算校准矩阵并写入libinput的主体程序 +usb_ds usb设备插拔监控 + +## 功能设计 + +### 执行设置的功能 + +首先关闭所有的监控程序可功能。 +执行设置时,先通过脚本准备环境,包括桌面环境的检查、文件路径的创建。 +然后执行 screen_binder 程序进行识别。 +完成后解析上个步骤生成的临时文件,将其补充信息后转换成我们可用的文件。 +最后拉起监控程序。 + + +### 检测功能 +由服务拉起脚本,脚本配置好环境后在用户环境运行主程序,主程序使用python编写。 +1. 准备环境、文件夹 +2. 首先启动屏幕监控,会实时更新screen.txt。 +3. 启动usb监控,会实时更新usbadd.txt。 +4. 发起一次校准操作。 +5. 程序死循环运行,除非收到关闭信号。 + 5.1 高频轮询screen.txt和usbadd.txt的更新 + 5.2 低频启动touch_ds,检测touchmap.txt和计算值的差异。 + 5.3 以上任一一个条件触发重新校准操作。 + + +### 校准功能 +1. 读取配置文件并解析 +2. 读取screen.txt,并对齐 +3. 计算校准矩阵 +4. 将校准写入系统 + + +编译环境 +sudo apt install libinput-tools python3-evdev python3-pyudev -y +apt download libevdev-dev libinput-dev libmtdev-dev libudev-dev libwacom-dev libevdev2 libinput-bin libinput10 libudev1 libwacom-common libwacom2 udev + + +fix3更新 +程序完全兼容麒麟和uos,解决了大量麒麟上的显示bug。垃圾麒麟!! + + +fix4更新 +新增支持旋转方向适配,计算矩阵时将会考虑到屏幕的旋转。 +新增支持签字笔的配置,采用传统方式。 + +fix5更新 +新增UOS显示器设置工具 +新增旋转适配 +新增debug脚本用于现场快速检查 + diff --git a/opt/ktouch/src/screen_binder.c b/opt/ktouch/src/screen_binder.c new file mode 100644 index 0000000..9f27055 --- /dev/null +++ b/opt/ktouch/src/screen_binder.c @@ -0,0 +1,618 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "touch_listen.h" + +// 添加Xrandr头文件和X11显示类型头文件 +#ifdef GDK_WINDOWING_X11 +#include +#include +#endif + +// 全局变量 +GtkWidget **calibration_windows = NULL; +GtkWidget **info_windows = NULL; +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +int current_screen = 0; +int total_screens = 0; +int *screen_widths = NULL; +int *screen_heights = NULL; +int *screen_x_offsets = NULL; +int *screen_y_offsets = NULL; +char **screen_names = NULL; +int *touch_bindings = NULL; +char **touch_names = NULL; +char **touch_paths = NULL; +int touch_received = 0; +int enter_pressed = 0; +time_t screen_start_time = 0; +guint timeout_id = 0; +FILE *log_file = NULL; +int info_window_index = 0; // 用于跟踪信息窗口创建进度 +Display *xdisplay = NULL; // X11显示连接 + +// 使用原子操作确保线程安全的状态标志 +_Atomic int global_enter_pressed = 0; // 全局回车键按下标志 +_Atomic int program_active = 0; // 程序是否处于活动状态 +_Atomic int calibration_active = 0; // 校准是否正在进行 + +pthread_mutex_t enter_mutex = PTHREAD_MUTEX_INITIALIZER; // 回车键互斥锁 +pthread_t enter_thread; // 全局回车键监听线程 +pthread_t touch_thread; + +// 函数声明 +void show_next_screen(); +void touch_callback(struct touch_event event); +gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data); +gboolean on_timeout(gpointer user_data); +GtkWidget* create_info_window(int screen_index); +GtkWidget* create_calibration_window(int screen_index); +void on_calibration_window_destroy(GtkWidget *widget, gpointer user_data); +void get_screen_info(); +void* touch_listener_thread(void* arg); +void log_message(const char *format, ...); +void save_mapping_table(); +gboolean create_info_windows_timeout(gpointer user_data); +void* global_enter_listener_thread(void* arg); // 全局回车监听线程 +gboolean check_enter_pressed(gpointer user_data); // 检查回车键按下的超时函数 +void cleanup_resources(); // 清理资源函数 + +// 保存映射关系表到文件 +void save_mapping_table() { + FILE *map_file = fopen("touch_dis_table.txt", "w"); + if (!map_file) { + log_message("无法打开映射关系表文件 touch_dis_table.txt\n"); + return; + } + + log_message("保存映射关系到文件: touch_dis_table.txt\n"); + + for (int i = 0; i < total_screens; i++) { + if (touch_bindings[i] != -1) { + fprintf(map_file, "%s|%s|%d|%s\n", + screen_names[i], + touch_names[i], + touch_bindings[i], + touch_paths[i]); + } else { + fprintf(map_file, "%s| | | \n", screen_names[i]); + } + } + + fclose(map_file); + log_message("映射关系表保存完成\n"); +} + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + printf("[%s] screen_binder \t", time_str); + vprintf(format, args); + + if (log_file) { + fprintf(log_file, "[%s] screen_binder \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); + } + + va_end(args); +} + +// 显示错误对话框 +void show_error_dialog(const char *message) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "%s", message); + gtk_window_set_title(GTK_WINDOW(dialog), "错误"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +// 触摸事件回调函数 +void touch_callback(struct touch_event event) { + pthread_mutex_lock(&mutex); + if(event.event_type == 1){ + pthread_mutex_unlock(&mutex); + return; + } + const char *event_types[] = {"按下", "移动", "释放"}; + + log_message("触摸事件: 设备ID=%d, 设备名=%s, 设备路径=%s, 类型=%s, 坐标=(%d, %d)\n", + event.device_id, event.device_name, event.device_path, event_types[event.event_type], event.x, event.y); + + static int touch_down_received = 0; + if (event.event_type == 0) { + touch_down_received = 1; + log_message("按下事件记录\n"); + } + else if (event.event_type == 2 && touch_down_received) { + log_message("有效触摸序列完成 (按下+释放)\n"); + touch_bindings[current_screen] = event.device_id; + strncpy(touch_names[current_screen], event.device_name, 255); + touch_names[current_screen][255] = '\0'; + strncpy(touch_paths[current_screen], event.device_path, 255); + touch_paths[current_screen][255] = '\0'; + touch_received = 1; + touch_down_received = 0; + + g_idle_add((GSourceFunc)gtk_widget_destroy, calibration_windows[current_screen]); + } + + pthread_mutex_unlock(&mutex); +} + +// 键盘事件处理 +gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { + if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) { + log_message("用户按下回车键,跳过屏幕 %d (%s)\n", current_screen, screen_names[current_screen]); + gtk_widget_destroy(widget); + return TRUE; + } + return FALSE; +} + +// 超时处理函数 +gboolean on_timeout(gpointer user_data) { + log_message("屏幕 %d (%s) 超时60秒,继续下一个屏幕\n", current_screen, screen_names[current_screen]); + gtk_widget_destroy(calibration_windows[current_screen]); + timeout_id = 0; + return G_SOURCE_REMOVE; +} + +// 创建信息提示窗口 +GtkWidget* create_info_window(int screen_index) { + GdkRectangle rect = { + .x = screen_x_offsets[screen_index], + .y = screen_y_offsets[screen_index], + .width = screen_widths[screen_index], + .height = screen_heights[screen_index] + }; + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "屏幕校准"); + gtk_window_move(GTK_WINDOW(window), rect.x, rect.y); + gtk_window_set_default_size(GTK_WINDOW(window), rect.width, rect.height); + gtk_window_set_decorated(GTK_WINDOW(window), TRUE); + gtk_window_set_keep_above(GTK_WINDOW(window), TRUE); // 确保窗口在最前面 + + char message[512]; + snprintf(message, sizeof(message), + "正在校准其他屏幕,请根据其他屏幕上的信息指导操作。\n\n当前屏幕接口: %s", + screen_names[screen_index]); + + GtkWidget *label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), message); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + + // 添加容器使标签居中 + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(window), box); + + gtk_window_set_deletable(GTK_WINDOW(window), FALSE); + + return window; +} + +// 创建校准窗口 +GtkWidget* create_calibration_window(int screen_index) { + GdkRectangle rect = { + .x = screen_x_offsets[screen_index], + .y = screen_y_offsets[screen_index], + .width = screen_widths[screen_index], + .height = screen_heights[screen_index] + }; + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "触摸屏校准"); + gtk_window_move(GTK_WINDOW(window), rect.x, rect.y); + gtk_window_set_default_size(GTK_WINDOW(window), rect.width, rect.height); + gtk_window_fullscreen(GTK_WINDOW(window)); + gtk_window_set_decorated(GTK_WINDOW(window), TRUE); + gtk_window_set_keep_above(GTK_WINDOW(window), TRUE); // 确保窗口在最前面 + gtk_window_set_modal(GTK_WINDOW(window), TRUE); // 设置为模态窗口,防止失去焦点 + + // 设置窗口类型提示为对话框,增加获得焦点的机会 + gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DIALOG); + + // 强制窗口获得焦点 + gtk_window_present(GTK_WINDOW(window)); + + char message[512]; + snprintf(message, sizeof(message), + "请在触摸屏上点击此屏幕。\n\n屏幕接口: %s\n\n如果此屏幕不是触摸屏,那么请敲击回车或者等待60秒。\n\n点击后请查看下一个屏幕,请勿重复点击。", + screen_names[screen_index]); + + GtkWidget *label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), message); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + + // 添加容器使标签居中 + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(window), box); + + g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL); + + return window; +} + +// 窗口关闭事件处理 +void on_calibration_window_destroy(GtkWidget *widget, gpointer user_data) { + log_message("关闭屏幕 %d (%s) 的校准窗口\n", current_screen, screen_names[current_screen]); + + // 取消超时计时器 + if (timeout_id > 0) { + g_source_remove(timeout_id); + timeout_id = 0; + } + + // 如果所有屏幕都已处理完毕 + if (++current_screen >= total_screens) { + // 设置程序状态为非活动 + atomic_store(&program_active, 0); + atomic_store(&calibration_active, 0); + + // 首先,销毁所有信息窗口 + for (int i = 0; i < total_screens; i++) { + if (info_windows[i] != NULL) { + gtk_widget_destroy(info_windows[i]); + info_windows[i] = NULL; + } + } + + // 打印绑定结果 + log_message("\n=== 绑定结果 ===\n"); + for (int i = 0; i < total_screens; i++) { + log_message("屏幕 %d (%s): ", i, screen_names[i]); + if (touch_bindings[i] != -1) { + log_message("触摸设备ID: %d, 设备名: %s, 设备路径: %s\n", touch_bindings[i], touch_names[i], touch_paths[i]); + } else { + log_message("未绑定触摸设备\n"); + } + } + + // 保存映射关系表 + save_mapping_table(); + + // 清理资源 + cleanup_resources(); + + gtk_main_quit(); + } else { + // 显示下一个屏幕的窗口 + show_next_screen(); + } +} + +// 超时创建信息窗口的回调函数 +gboolean create_info_windows_timeout(gpointer user_data) { + if (info_window_index < total_screens) { + info_windows[info_window_index] = create_info_window(info_window_index); + gtk_widget_show_all(info_windows[info_window_index]); + log_message("为屏幕 %d (%s) 创建信息窗口\n", info_window_index, screen_names[info_window_index]); + info_window_index++; + return TRUE; // 继续调用 + } else { + // 所有信息窗口创建完毕,开始校准流程 + show_next_screen(); + return FALSE; // 停止调用 + } +} + +// 显示下一个屏幕的窗口 +void show_next_screen() { + log_message("\n=== 处理屏幕 %d (%s) ===\n", current_screen, screen_names[current_screen]); + + // 设置校准状态为活动 + atomic_store(&calibration_active, 1); + + // 创建当前屏幕的校准窗口 + calibration_windows[current_screen] = create_calibration_window(current_screen); + g_signal_connect(calibration_windows[current_screen], "destroy", + G_CALLBACK(on_calibration_window_destroy), NULL); + gtk_widget_show_all(calibration_windows[current_screen]); + + // 强制窗口获得焦点 + gtk_window_present(GTK_WINDOW(calibration_windows[current_screen])); + + log_message("创建校准窗口完成\n"); + + // 重置状态 + pthread_mutex_lock(&mutex); + touch_received = 0; + enter_pressed = 0; + screen_start_time = time(NULL); + pthread_mutex_unlock(&mutex); + + // 设置60秒超时 + timeout_id = g_timeout_add_seconds(60, on_timeout, NULL); + log_message("显示提示文本完成,开始60秒计时\n"); +} + +// 获取屏幕信息 (使用Xrandr方法) +void get_screen_info() { + GdkDisplay *gdk_display = gdk_display_get_default(); + + #ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY(gdk_display)) { + log_message("错误: 非X11显示环境\n"); + return; + } + + xdisplay = GDK_DISPLAY_XDISPLAY(gdk_display); + Window xroot = GDK_WINDOW_XID(gdk_get_default_root_window()); + + int rr_event_base, rr_error_base; + if (!XRRQueryExtension(xdisplay, &rr_event_base, &rr_error_base)) { + log_message("错误: XRandR扩展不可用\n"); + return; + } + + XRRScreenResources *screen_res = XRRGetScreenResourcesCurrent(xdisplay, xroot); + if (!screen_res) { + log_message("错误: 无法获取屏幕资源\n"); + return; + } + + // 计算连接的显示器数量 + total_screens = 0; + for (int i = 0; i < screen_res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + total_screens++; + } + if (output_info) XRRFreeOutputInfo(output_info); + } + + log_message("找到 %d 个连接的显示器\n", total_screens); + + // 分配内存存储屏幕信息 + screen_widths = malloc(total_screens * sizeof(int)); + screen_heights = malloc(total_screens * sizeof(int)); + screen_x_offsets = malloc(total_screens * sizeof(int)); + screen_y_offsets = malloc(total_screens * sizeof(int)); + screen_names = malloc(total_screens * sizeof(char*)); + touch_bindings = malloc(total_screens * sizeof(int)); + touch_names = malloc(total_screens * sizeof(char*)); + touch_paths = malloc(total_screens * sizeof(char*)); + calibration_windows = malloc(total_screens * sizeof(GtkWidget*)); + info_windows = malloc(total_screens * sizeof(GtkWidget*)); + + // 获取每个显示器的详细信息 + int screen_index = 0; + for (int i = 0; i < screen_res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + if (output_info->crtc) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(xdisplay, screen_res, output_info->crtc); + if (crtc_info) { + screen_names[screen_index] = strdup(output_info->name); + screen_widths[screen_index] = crtc_info->width; + screen_heights[screen_index] = crtc_info->height; + screen_x_offsets[screen_index] = crtc_info->x; + screen_y_offsets[screen_index] = crtc_info->y; + + touch_bindings[screen_index] = -1; + touch_names[screen_index] = malloc(256 * sizeof(char)); + strcpy(touch_names[screen_index], "未绑定"); + touch_paths[screen_index] = malloc(256 * sizeof(char)); + strcpy(touch_paths[screen_index], "/dev/null"); + calibration_windows[screen_index] = NULL; + info_windows[screen_index] = NULL; + + log_message("显示器 %d: %s, 分辨率: %dx%d, 位置: %dx%d\n", + screen_index, screen_names[screen_index], + screen_widths[screen_index], screen_heights[screen_index], + screen_x_offsets[screen_index], screen_y_offsets[screen_index]); + + XRRFreeCrtcInfo(crtc_info); + screen_index++; + } + } + } + if (output_info) XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(screen_res); + #else + log_message("错误: 非X11环境,无法使用Xrandr\n"); + #endif +} + +// 触摸监听线程 +void* touch_listener_thread(void* arg) { + log_message("开始监听触摸事件...\n"); + listen_touch_events(); + return NULL; +} + +// 全局回车键监听线程 +void* global_enter_listener_thread(void* arg) { + log_message("开始全局监听回车键...\n"); + + // 打开X11显示连接 + Display *display = XOpenDisplay(NULL); + if (!display) { + log_message("无法打开X11显示连接以监听全局回车键\n"); + return NULL; + } + + // 获取根窗口 + Window root = DefaultRootWindow(display); + + // 抓取回车键 + XGrabKey(display, XKeysymToKeycode(display, XK_Return), AnyModifier, + root, False, GrabModeAsync, GrabModeAsync); + XGrabKey(display, XKeysymToKeycode(display, XK_KP_Enter), AnyModifier, + root, False, GrabModeAsync, GrabModeAsync); + + XEvent event; + while (atomic_load(&program_active)) { + // 使用非阻塞方式检查事件 + if (XPending(display) > 0) { + XNextEvent(display, &event); + if (event.type == KeyPress) { + KeySym keysym = XLookupKeysym(&event.xkey, 0); + if (keysym == XK_Return || keysym == XK_KP_Enter) { + // 只有在校准活动时才响应回车键 + if (atomic_load(&calibration_active)) { + log_message("全局回车键被按下(校准过程中)\n"); + atomic_store(&global_enter_pressed, 1); + } else { + log_message("全局回车键被按下(非校准过程中,忽略)\n"); + } + } + } + } else { + // 短暂睡眠以减少CPU使用 + usleep(100000); // 100毫秒 + } + } + + // 释放抓取的键 + XUngrabKey(display, XKeysymToKeycode(display, XK_Return), AnyModifier, root); + XUngrabKey(display, XKeysymToKeycode(display, XK_KP_Enter), AnyModifier, root); + XCloseDisplay(display); + log_message("全局回车键监听线程退出\n"); + return NULL; +} + +// 检查回车键按下的超时函数 +gboolean check_enter_pressed(gpointer user_data) { + if (atomic_load(&global_enter_pressed)) { + atomic_store(&global_enter_pressed, 0); + + // 只有在校准活动时才处理回车键 + if (atomic_load(&calibration_active) && calibration_windows[current_screen]) { + log_message("检测到全局回车键按下,跳过屏幕 %d (%s)\n", current_screen, screen_names[current_screen]); + g_idle_add((GSourceFunc)gtk_widget_destroy, calibration_windows[current_screen]); + } + } + + return G_SOURCE_CONTINUE; // 继续调用 +} + +// 清理资源函数 +void cleanup_resources() { + + // 停止触摸监听 + stop_touch_listener(); + + // // 等待触摸线程结束 + // if (touch_thread) { + // pthread_join(touch_thread, NULL); + // touch_thread = 0; + // } + + // 清理屏幕信息相关资源 + for (int i = 0; i < total_screens; i++) { + free(screen_names[i]); + free(touch_names[i]); + free(touch_paths[i]); + } + free(screen_widths); + free(screen_heights); + free(screen_x_offsets); + free(screen_y_offsets); + free(screen_names); + free(touch_bindings); + free(touch_names); + free(touch_paths); + free(calibration_windows); + free(info_windows); + + // 关闭X11显示连接 + // if (xdisplay) { + // XCloseDisplay(xdisplay); + // xdisplay = NULL; + // } + + // 关闭日志文件 + if (log_file) { + fclose(log_file); + log_file = NULL; + } +} + +int main(int argc, char *argv[]) { + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + log_message("=== 触摸屏与显示器绑定程序 (使用Xrandr获取屏幕信息) ===\n"); + + // 设置程序状态为活动 + atomic_store(&program_active, 1); + atomic_store(&calibration_active, 0); + + gtk_init(&argc, &argv); + + get_screen_info(); + + log_message("初始化触摸监听器...\n"); + int rst = init_touch_listener(touch_callback); + if(rst){ + log_message("未找到触摸设备或无法打开触摸设备,请使用sudo运行。\n"); + show_error_dialog("未找到触摸设备或无法打开触摸设备,请使用sudo运行。\n"); + log_message("程序结束\n"); + cleanup_resources(); + return 0; + } + + // pthread_t touch_thread; + touch_thread = 0; + if (pthread_create(&touch_thread, NULL, touch_listener_thread, NULL) != 0) { + log_message("无法创建触摸监听线程\n"); + cleanup_resources(); + return 1; + } + + // pthread_detach(touch_thread); + + // 创建全局回车键监听线程 + if (pthread_create(&enter_thread, NULL, global_enter_listener_thread, NULL) != 0) { + log_message("无法创建全局回车键监听线程\n"); + } + + // 添加定时器检查回车键按下 + g_timeout_add(100, check_enter_pressed, NULL); // 每100毫秒检查一次 + + // 使用超时函数依次创建信息窗口,间隔100毫秒 + info_window_index = 0; + g_timeout_add(10, create_info_windows_timeout, NULL); + + gtk_main(); + + // 设置程序状态为非活动,等待线程退出 + atomic_store(&program_active, 0); + + // 等待全局回车键监听线程退出 + pthread_join(enter_thread, NULL); + + log_message("程序结束\n"); + return 0; +} \ No newline at end of file diff --git a/opt/ktouch/src/screen_ds.c b/opt/ktouch/src/screen_ds.c new file mode 100644 index 0000000..95a646f --- /dev/null +++ b/opt/ktouch/src/screen_ds.c @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBOUNCE_TIME 3 // 防抖时间(秒) + +typedef struct { + char* output_name; + int width; + int height; + int x; + int y; + int rotation; // 新增:旋转方向 +} MonitorInfo; + +typedef struct { + MonitorInfo* monitors; + int count; +} MonitorList; + +FILE *log_file = NULL; // 日志文件指针 + + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] screen_ds \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] screen_ds \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + +// 获取当前时间戳(毫秒) +long long current_timestamp() { + struct timeval te; + gettimeofday(&te, NULL); + long long milliseconds = te.tv_sec * 1000LL + te.tv_usec / 1000; + return milliseconds; +} + +// 将旋转值转换为单个字母表示 +char rotation_to_char(int rotation) { + switch (rotation) { + case RR_Rotate_0: return 'N'; // 正常 + case RR_Rotate_90: return 'L'; // 左旋转 + case RR_Rotate_180: return 'I'; // 倒转 + case RR_Rotate_270: return 'R'; // 右旋转 + default: return 'N'; // 未知则也是正常方向 + } +} + +// 获取显示器信息 +MonitorList get_monitor_info(Display* dpy, Window root) { + MonitorList list = {NULL, 0}; + XRRScreenResources *res = XRRGetScreenResourcesCurrent(dpy, root); + if (!res) { + log_message( "无法获取屏幕资源\n"); + return list; + } + + // 第一次遍历计算连接中的显示器数量 + int connected_count = 0; + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + connected_count++; + } + XRRFreeOutputInfo(output_info); + } + + // 分配内存 + list.monitors = malloc(connected_count * sizeof(MonitorInfo)); + list.count = 0; + + // 第二次遍历获取详细信息 + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + if (output_info->crtc) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(dpy, res, output_info->crtc); + if (crtc_info) { + list.monitors[list.count].output_name = strdup(output_info->name); + list.monitors[list.count].width = crtc_info->width; + list.monitors[list.count].height = crtc_info->height; + list.monitors[list.count].x = crtc_info->x; + list.monitors[list.count].y = crtc_info->y; + list.monitors[list.count].rotation = crtc_info->rotation; // 新增:记录旋转方向 + list.count++; + XRRFreeCrtcInfo(crtc_info); + } + } + } + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(res); + return list; +} + +// 释放显示器列表内存 +void free_monitor_list(MonitorList* list) { + for (int i = 0; i < list->count; i++) { + free(list->monitors[i].output_name); + } + free(list->monitors); + list->monitors = NULL; + list->count = 0; +} + +// 输出显示器信息到文件或标准输出 +void output_monitor_info(MonitorList* list, const char* filename) { + FILE* output = stdout; + + if (filename) { + output = fopen(filename, "w"); + if (!output) { + log_message("无法打开目标文件: %s\n", filename); + output = stdout; + } + } + + log_message("检测到显示器:\n"); + for (int i = 0; i < list->count; i++) { + fprintf(output, "%s|%dx%d|%dx%d|%c\n", + list->monitors[i].output_name, + list->monitors[i].width, + list->monitors[i].height, + list->monitors[i].x, + list->monitors[i].y, + rotation_to_char(list->monitors[i].rotation)); + log_message(" %s:分辨率%dx%d, 位置%dx%d, 旋转:%c\n", + list->monitors[i].output_name, + list->monitors[i].width, + list->monitors[i].height, + list->monitors[i].x, + list->monitors[i].y, + rotation_to_char(list->monitors[i].rotation)); + } + + if (output != stdout) { + fclose(output); + log_message("显示器信息已保存到: %s\n", filename); + } +} + +int main(int argc, char *argv[]) { + + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + log_message("=== 显示器监控程序 ===\n"); + + + + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + log_message("无法打开X显示\n"); + return 1; + } + + int rr_event_base, rr_error_base; + if (!XRRQueryExtension(dpy, &rr_event_base, &rr_error_base)) { + log_message("RandR扩展未找到\n"); + XCloseDisplay(dpy); + return 1; + } + + Window root = DefaultRootWindow(dpy); + // 监听屏幕变化和输出变化事件 + XRRSelectInput(dpy, root, RRScreenChangeNotifyMask | RROutputChangeNotifyMask); + + // 获取命令行参数 + const char* output_file = NULL; + if (argc > 1) { + output_file = argv[1]; + } + + int need_update = 0; + long long last_event_time = 0; + + printf("开始监听显示器变化...\n"); + printf("使用Ctrl+C退出程序\n"); + if (output_file) { + printf("结果将保存到: %s\n", output_file); + } else { + printf("结果将输出到标准输出\n"); + } + + // 保存首次信息 + MonitorList monitors = get_monitor_info(dpy, root); + output_monitor_info(&monitors, output_file); + free_monitor_list(&monitors); + + XEvent ev; + while (1) { + // 检查是否有事件,但设置超时以避免阻塞 + if (XPending(dpy)) { + XNextEvent(dpy, &ev); + + // 处理RandR事件 + if (ev.type == rr_event_base + RRScreenChangeNotify || + ev.type == rr_event_base + RRNotify) { + need_update = 1; + last_event_time = current_timestamp(); + } + } else { + // 没有事件时检查是否需要更新 + long long current_time = current_timestamp(); + if (need_update && (current_time - last_event_time) >= DEBOUNCE_TIME * 1000) { + MonitorList monitors = get_monitor_info(dpy, root); + output_monitor_info(&monitors, output_file); + free_monitor_list(&monitors); + need_update = 0; + } + + // 短暂休眠以减少CPU使用 + usleep(100000); // 100ms + } + } + + XCloseDisplay(dpy); + return 0; +} \ No newline at end of file diff --git a/opt/ktouch/src/screen_ds_once.c b/opt/ktouch/src/screen_ds_once.c new file mode 100644 index 0000000..40cc3a9 --- /dev/null +++ b/opt/ktouch/src/screen_ds_once.c @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBOUNCE_TIME 3 // 防抖时间(秒) + +typedef struct { + char* output_name; + int width; + int height; + int x; + int y; + int rotation; // 新增:旋转方向 +} MonitorInfo; + +typedef struct { + MonitorInfo* monitors; + int count; +} MonitorList; + +FILE *log_file = NULL; // 日志文件指针 + + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] screen_ds_once \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] screen_ds \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + +// 获取当前时间戳(毫秒) +long long current_timestamp() { + struct timeval te; + gettimeofday(&te, NULL); + long long milliseconds = te.tv_sec * 1000LL + te.tv_usec / 1000; + return milliseconds; +} + +// 将旋转值转换为单个字母表示 +char rotation_to_char(int rotation) { + switch (rotation) { + case RR_Rotate_0: return 'N'; // 正常 + case RR_Rotate_90: return 'L'; // 左旋转 + case RR_Rotate_180: return 'I'; // 倒转 + case RR_Rotate_270: return 'R'; // 右旋转 + default: return 'N'; // 未知则也是正常方向 + } +} + +// 获取显示器信息 +MonitorList get_monitor_info(Display* dpy, Window root) { + MonitorList list = {NULL, 0}; + XRRScreenResources *res = XRRGetScreenResourcesCurrent(dpy, root); + if (!res) { + log_message( "无法获取屏幕资源\n"); + return list; + } + + // 第一次遍历计算连接中的显示器数量 + int connected_count = 0; + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + connected_count++; + } + XRRFreeOutputInfo(output_info); + } + + // 分配内存 + list.monitors = malloc(connected_count * sizeof(MonitorInfo)); + list.count = 0; + + // 第二次遍历获取详细信息 + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *output_info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (output_info && output_info->connection == RR_Connected) { + if (output_info->crtc) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(dpy, res, output_info->crtc); + if (crtc_info) { + list.monitors[list.count].output_name = strdup(output_info->name); + list.monitors[list.count].width = crtc_info->width; + list.monitors[list.count].height = crtc_info->height; + list.monitors[list.count].x = crtc_info->x; + list.monitors[list.count].y = crtc_info->y; + list.monitors[list.count].rotation = crtc_info->rotation; // 新增:记录旋转方向 + list.count++; + XRRFreeCrtcInfo(crtc_info); + } + } + } + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(res); + return list; +} + +// 释放显示器列表内存 +void free_monitor_list(MonitorList* list) { + for (int i = 0; i < list->count; i++) { + free(list->monitors[i].output_name); + } + free(list->monitors); + list->monitors = NULL; + list->count = 0; +} + +// 输出显示器信息到文件或标准输出 +void output_monitor_info(MonitorList* list, const char* filename) { + FILE* output = stdout; + + if (filename) { + output = fopen(filename, "w"); + if (!output) { + log_message("无法打开目标文件: %s\n", filename); + output = stdout; + } + } + + log_message("检测到显示器:\n"); + for (int i = 0; i < list->count; i++) { + fprintf(output, "%s|%dx%d|%dx%d|%c\n", + list->monitors[i].output_name, + list->monitors[i].width, + list->monitors[i].height, + list->monitors[i].x, + list->monitors[i].y, + rotation_to_char(list->monitors[i].rotation)); + log_message(" %s:分辨率%dx%d, 位置%dx%d, 旋转:%c\n", + list->monitors[i].output_name, + list->monitors[i].width, + list->monitors[i].height, + list->monitors[i].x, + list->monitors[i].y, + rotation_to_char(list->monitors[i].rotation)); + } + + if (output != stdout) { + fclose(output); + log_message("显示器信息已保存到: %s\n", filename); + } +} + +int main(int argc, char *argv[]) { + + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + log_message("=== 显示器监控程序(单次执行) ===\n"); + + + + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + log_message("无法打开X显示\n"); + return 1; + } + + int rr_event_base, rr_error_base; + if (!XRRQueryExtension(dpy, &rr_event_base, &rr_error_base)) { + log_message("RandR扩展未找到\n"); + XCloseDisplay(dpy); + return 1; + } + + Window root = DefaultRootWindow(dpy); + // 监听屏幕变化和输出变化事件 + //XRRSelectInput(dpy, root, RRScreenChangeNotifyMask | RROutputChangeNotifyMask); + + // 获取命令行参数 + const char* output_file = NULL; + if (argc > 1) { + output_file = argv[1]; + } + + int need_update = 0; + long long last_event_time = 0; + + //printf("开始监听显示器变化...\n"); + //printf("使用Ctrl+C退出程序\n"); + if (output_file) { + printf("结果将保存到: %s\n", output_file); + } else { + printf("结果将输出到标准输出\n"); + } + + // 保存首次信息 + MonitorList monitors = get_monitor_info(dpy, root); + output_monitor_info(&monitors, output_file); + free_monitor_list(&monitors); + + //XEvent ev; + + + XCloseDisplay(dpy); + return 0; +} \ No newline at end of file diff --git a/opt/ktouch/src/touch_dis_table.txt b/opt/ktouch/src/touch_dis_table.txt new file mode 100644 index 0000000..4023720 --- /dev/null +++ b/opt/ktouch/src/touch_dis_table.txt @@ -0,0 +1,2 @@ +HDMI-0|ILITEK Multi-Touch-V5000|1435117611|/dev/input/event10 +VGA-0|ILITEK ILITEK-TP|1435117617|/dev/input/event16 diff --git a/opt/ktouch/src/touch_ds.c b/opt/ktouch/src/touch_ds.c new file mode 100644 index 0000000..2808c11 --- /dev/null +++ b/opt/ktouch/src/touch_ds.c @@ -0,0 +1,604 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 监控等待时间 +int watch_time_delay = 30; + + + +FILE *log_file = NULL; // 日志文件指针 +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] touch_ds \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] touch_ds \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + +// 清理字符串中可能存在的空格 +void clean_string(const char* input, char* output, size_t output_size) { + if (!input || !output || output_size == 0) { + if (output && output_size > 0) { + output[0] = '\0'; + } + return; + } + + // 第一步:去除所有空格 + char temp[256] = {0}; + size_t temp_idx = 0; + + for (size_t i = 0; input[i] != '\0' && temp_idx < sizeof(temp) - 1; i++) { + if (!isspace((unsigned char)input[i])) { + temp[temp_idx++] = input[i]; + } + } + temp[temp_idx] = '\0'; + + // 第二步:去除首尾引号 + const char* start = temp; + const char* end = temp + strlen(temp) - 1; + + if (*start == '"' || *start == '\'') { + start++; + } + if (end >= start && (*end == '"' || *end == '\'')) { + end--; + } + + // 第三步:复制到输出缓冲区 + size_t len = end - start + 1; + if (len >= output_size) { + len = output_size - 1; + } + + strncpy(output, start, len); + output[len] = '\0'; +} + + +// 获取FLOAT原子类型 +Atom get_float_atom(Display* display) { + return XInternAtom(display, "FLOAT", False); +} + +// 获取STRING原子类型 +Atom get_string_atom(Display* display) { + return XInternAtom(display, "STRING", False); +} + +// 获取INTEGER原子类型 +Atom get_integer_atom(Display* display) { + return XInternAtom(display, "INTEGER", False); +} + +// 获取设备属性值 +char* get_device_property(Display* display, XID deviceid, const char* prop_name, Atom expected_type) { + Atom prop = XInternAtom(display, prop_name, False); + Atom type; + int format; + unsigned long nitems; + unsigned long bytes_after; + unsigned char* data = NULL; + + // 获取设备属性 + int result = XIGetProperty(display, deviceid, prop, 0, 1024, False, + AnyPropertyType, &type, &format, + &nitems, &bytes_after, &data); + + if (result == Success && type == expected_type && data != NULL) { + if (format == 8) { // 字符串类型 + return strdup((char*)data); + } + } + + if (data) XFree(data); + return NULL; +} + +// 获取设备矩阵属性 +float* get_device_matrix_property(Display* display, XID deviceid, const char* prop_name, int* success) { + Atom prop = XInternAtom(display, prop_name, False); + Atom float_atom = get_float_atom(display); + Atom type; + int format; + unsigned long nitems; + unsigned long bytes_after; + unsigned char* data = NULL; + + // 获取设备属性 + int result = XIGetProperty(display, deviceid, prop, 0, 9, False, + AnyPropertyType, &type, &format, + &nitems, &bytes_after, &data); + + if (result == Success && type == float_atom && format == 32 && nitems == 9) { + *success = 1; + return (float*)data; + } + + if (data) XFree(data); + *success = 0; + return NULL; +} + +// 判断设备是否存在 +// 检查设备是否存在 +int device_exists(Display* display, XID deviceid) { + int ndevices; + XIDeviceInfo* devices = XIQueryDevice(display, XIAllDevices, &ndevices); + + if (!devices) { + return 0; // 无法获取设备列表 + } + + int exists = 0; + for (int i = 0; i < ndevices; i++) { + if (devices[i].deviceid == deviceid) { + exists = 1; + break; + } + } + + XIFreeDeviceInfo(devices); + return exists; +} + + +// 获取设备矩阵属性字符串表示 +int get_matrix_string(Display* display, XID deviceid, char* result) { + // 第一步检查设备 + // 首先检查设备是否存在 + if (!device_exists(display, deviceid)) { + return 0; + } + int success; + float* matrix = NULL; + // char* result = malloc(256); // 分配足够空间存储矩阵字符串 + // log_message("尝试获取设备%d的校准矩阵\n", deviceid); + + // 先尝试获取 Coordinate Transformation Matrix + matrix = get_device_matrix_property(display, deviceid, + "Coordinate Transformation Matrix", &success); + + // 如果没有,尝试获取 libinput Calibration Matrix + if (!success) { + matrix = get_device_matrix_property(display, deviceid, + "libinput Calibration Matrix", &success); + } + + if (success && matrix) { + // 格式化矩阵字符串,保留6位小数,去掉括号 + sprintf(result, "%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f", + matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5], + matrix[6], matrix[7], matrix[8]); + XFree(matrix); + } else { + strcpy(result, "None"); + } + return 1; +} + +// 获取设备节点信息 +char* get_device_node(Display* display, XID deviceid, const char* device_name) { + // 尝试不同的属性名称来获取设备节点 + char* device_node = NULL; + Atom string_atom = get_string_atom(display); + + // 尝试不同的属性名称 + const char* prop_names[] = { + "Device Node", + "device-node", + "DEVICE_NODE", + NULL + }; + + for (int i = 0; prop_names[i] != NULL; i++) { + device_node = get_device_property(display, deviceid, prop_names[i], string_atom); + if (device_node != NULL) { + // printf(" 找到设备节点: %s (属性: %s)\n", device_node, prop_names[i]); + break; + } + } + + if (device_node == NULL) { + device_node = strdup("Unknown"); + // printf(" 未能找到设备节点,使用 'Unknown'\n"); + } + + return device_node; +} + +// 检查文件是否存在 +int file_exists(const char *path) { + struct stat buf; + return (stat(path, &buf) == 0); +} + +// 使用udev检查设备是否为触摸屏并获取VID:PID +int is_touchscreen_device(const char* device_node, char* vid, char* pid, int buffer_size) { + if (strcmp(device_node, "Unknown") == 0) { + // printf(" 设备节点未知,跳过触摸屏检查\n"); + return 0; + } + + // 首先检查设备节点是否存在 + if (!file_exists(device_node)) { + // printf(" 设备节点不存在: %s\n", device_node); + return 0; + } + + struct udev *udev; + struct udev_device *dev; + int is_touchscreen = 0; + + // 初始化VID和PID + strncpy(vid, "Unknown", buffer_size); + strncpy(pid, "Unknown", buffer_size); + + // 创建udev上下文 + udev = udev_new(); + if (!udev) { + log_message("无法创建udev上下文\n"); + return 0; + } + + // printf(" 正在检查设备: %s\n", device_node); + + // 获取设备信息 + dev = udev_device_new_from_syspath(udev, device_node); + if (!dev) { + log_message(" 无法从syspath创建udev设备: %s\n", device_node); + + // 尝试通过设备节点创建udev设备 + struct stat st; + if (stat(device_node, &st) == 0) { + dev = udev_device_new_from_devnum(udev, 'c', st.st_rdev); + // if (dev) { + // printf(" 通过设备节点创建udev设备成功\n"); + // } else { + // printf(" 通过设备节点创建udev设备失败\n"); + // } + } else { + log_message(" 无法获取设备状态: %s\n", device_node); + } + } + + if (dev) { + // 打印设备属性 + // printf(" 设备属性:\n"); + struct udev_list_entry *properties = udev_device_get_properties_list_entry(dev); + struct udev_list_entry *entry; + + udev_list_entry_foreach(entry, properties) { + const char *name = udev_list_entry_get_name(entry); + const char *value = udev_list_entry_get_value(entry); + if (strstr(name, "INPUT") || strstr(name, "ID_")) { + // printf(" %s=%s\n", name, value); + + // 获取VID和PID + if (strcmp(name, "ID_VENDOR_ID") == 0) { + strncpy(vid, value, buffer_size); + // printf(" 找到VID: %s\n", vid); + } else if (strcmp(name, "ID_MODEL_ID") == 0) { + strncpy(pid, value, buffer_size); + // printf(" 找到PID: %s\n", pid); + } + } + } + + // 向上遍历设备树,查找输入设备属性 + struct udev_device *parent = dev; + int level = 0; + while (parent && level < 10) { // 限制遍历深度 + // 检查ID_INPUT_TOUCHSCREEN属性 + const char *touchscreen = udev_device_get_property_value(parent, "ID_INPUT_TOUCHSCREEN"); + if (touchscreen) { + // printf(" 在层级 %d 找到 ID_INPUT_TOUCHSCREEN=%s\n", level, touchscreen); + if (strcmp(touchscreen, "1") == 0) { + is_touchscreen = 1; + + // 获取VID和PID + const char *vendor_id = udev_device_get_property_value(parent, "ID_VENDOR_ID"); + const char *model_id = udev_device_get_property_value(parent, "ID_MODEL_ID"); + + if (vendor_id) { + strncpy(vid, vendor_id, buffer_size); + // printf(" 找到VID: %s\n", vid); + } + + if (model_id) { + strncpy(pid, model_id, buffer_size); + // printf(" 找到PID: %s\n", pid); + } + + break; + } + } + + // 检查其他相关属性 + const char *capabilities = udev_device_get_property_value(parent, "ID_INPUT"); + if (capabilities) { + // printf(" 在层级 %d 找到 ID_INPUT=%s\n", level, capabilities); + if (strstr(capabilities, "touchscreen")) { + is_touchscreen = 1; + + // 获取VID和PID + const char *vendor_id = udev_device_get_property_value(parent, "ID_VENDOR_ID"); + const char *model_id = udev_device_get_property_value(parent, "ID_MODEL_ID"); + + if (vendor_id) { + strncpy(vid, vendor_id, buffer_size); + // printf(" 找到VID: %s\n", vid); + } + + if (model_id) { + strncpy(pid, model_id, buffer_size); + // printf(" 找到PID: %s\n", pid); + } + + break; + } + } + + // 检查父设备 + parent = udev_device_get_parent(parent); + level++; + } + + if (!is_touchscreen) { + // printf(" 未找到 ID_INPUT_TOUCHSCREEN=1 属性\n"); + } + + udev_device_unref(dev); + } else { + // printf(" 无法创建udev设备对象\n"); + } + + udev_unref(udev); + return is_touchscreen; +} + +// 从文件中读取触摸屏信息 +typedef struct { + XID deviceid; + char name[100]; + char node[100]; + char vid_pid[20]; + char matrix[120]; +} TouchscreenInfo; + +int read_touchscreens_from_file(const char* filename, TouchscreenInfo *touchscreens) { + int count = 0; + int max_count = 25; + FILE* file = fopen(filename, "r"); + if (!file) { + log_message("无法打开文件: %s\n", filename); + return 0; + } + + char line[1024]; + while (fgets(line, sizeof(line), file) && count < max_count) { + // 移除行尾的换行符 + line[strcspn(line, "\n")] = '\0'; + + // 跳过空行 + if (strlen(line) == 0) { + continue; + } + + // 使用strtok分割字符串 + char* token = strtok(line, "|"); + if (!token) continue; + + // 解析设备ID + touchscreens[count].deviceid = strtoul(token, NULL, 10); + + // 解析名称 + token = strtok(NULL, "|"); + if (!token) continue; + strncpy(touchscreens[count].name, token, sizeof(touchscreens[count].name) - 1); + touchscreens[count].name[sizeof(touchscreens[count].name) - 1] = '\0'; // 确保字符串终止 + + // 解析设备节点 + token = strtok(NULL, "|"); + if (!token) continue; + strncpy(touchscreens[count].node, token, sizeof(touchscreens[count].node) - 1); + touchscreens[count].node[sizeof(touchscreens[count].node) - 1] = '\0'; + + // 解析VID:PID + token = strtok(NULL, "|"); + if (!token) continue; + strncpy(touchscreens[count].vid_pid, token, sizeof(touchscreens[count].vid_pid) - 1); + touchscreens[count].vid_pid[sizeof(touchscreens[count].vid_pid) - 1] = '\0'; + + // 解析矩阵 + token = strtok(NULL, "|"); + if (!token) continue; + char matrix_with_space[120]; + strncpy(matrix_with_space, token, sizeof(touchscreens[count].matrix) - 1); + clean_string(matrix_with_space, touchscreens[count].matrix, sizeof(matrix_with_space)); + // touchscreens[count].matrix[sizeof(touchscreens[count].matrix) - 1] = '\0'; + + count++; + } + + fclose(file); + return count; +} + + +// 检查flag文件是否为1 +int is_flag_set(const char* flagfile) { + if (!flagfile || !file_exists(flagfile)) { + return 0; + } + + FILE* file = fopen(flagfile, "r"); + if (!file) { + return 0; + } + + char value[2]; + fgets(value, sizeof(value), file); + fclose(file); + + return (value[0] == '1'); +} + +// 设置flag文件值 +void set_flag(const char* flagfile, int value) { + if (!flagfile) return; + + FILE* file = fopen(flagfile, "w"); + if (!file) { + log_message("无法打开flag文件: %s\n", flagfile); + return; + } + + fprintf(file, "%d", value); + fclose(file); + log_message("设置flag文件 %s 为 %d\n", flagfile, value); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "用法: %s <触摸屏信息文件> [flag文件]\n", argv[0]); + return 1; + } + + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + log_message("=== 触摸屏矩阵监控 ===\n"); + + + const char* touchscreen_file = argv[1]; + const char* flag_file = argc > 2 ? argv[2] : NULL; + + // 读取触摸屏信息文件 + int touchscreen_count = 0; + TouchscreenInfo touchscreens[20]; + while(1){ + touchscreen_count = read_touchscreens_from_file(touchscreen_file, touchscreens); + if (!touchscreens || touchscreen_count == 0) { + fprintf(stderr, "没有找到触摸屏信息或无法读取文件,等待10秒后重新开始\n"); + sleep(10); + continue; + }else break; + } + + + Display* display = XOpenDisplay(NULL); + if (!display) { + log_message("无法打开X显示\n"); + // free_touchscreens(touchscreens, touchscreen_count); + return 1; + } + + // 检查XInput扩展 + int opcode, event, error; + if (!XQueryExtension(display, "XInputExtension", &opcode, &event, &error)) { + log_message("X Input扩展不可用\n"); + XCloseDisplay(display); + // free_touchscreens(touchscreens, touchscreen_count); + return 1; + } + + // 主循环 + while (1) { + // 检查flag文件 + if (flag_file && is_flag_set(flag_file)) { + printf("检测暂停,flag文件值为1\n"); + sleep(10); + continue; + } + + int changed = 0; + + touchscreen_count = read_touchscreens_from_file(touchscreen_file, touchscreens); + if (!touchscreens || touchscreen_count == 0) { + fprintf(stderr, "没有找到触摸屏信息或无法读取文件,等待10秒后重新开始\n"); + sleep(10); + continue; + } + + + // 检查每个触摸屏设备的矩阵 + for (int i = 0; i < touchscreen_count; i++) { + TouchscreenInfo* info = &touchscreens[i]; + printf("检查设备 ID: %lu, 名称: %s\n", info->deviceid, info->name); + + // 获取当前矩阵,产生的数据已经不带空格了 + char current_matrix[100] = {0}; + int isDeviceExist = 0; + // char* current_matrix = get_matrix_string(display, info->deviceid); + isDeviceExist = get_matrix_string(display, info->deviceid, current_matrix); + if(!isDeviceExist) { + printf("设备%lu不存在\n", info->deviceid); + continue; + } + + + + // 比较矩阵 + if (strcmp(current_matrix, info->matrix) != 0) { + log_message("设备 %lu 的矩阵发生变化:原矩阵: %s, 新矩阵: %s\n", info->deviceid,info->matrix,current_matrix); + changed = 1; + } + // free(current_matrix); + + + + // 如果检测到变化,设置flag并跳出循环 + if (changed) { + if (flag_file) { + set_flag(flag_file, 1); + } + break; + } + } + + if (!changed) { + // printf("所有触摸屏设备矩阵未发生变化\n"); + } + + // 等待10秒 + sleep(watch_time_delay); + } + + // 清理资源(实际上不会执行到这里,因为上面是无限循环) + // free_touchscreens(touchscreens, touchscreen_count); + XCloseDisplay(display); + + return 0; +} \ No newline at end of file diff --git a/opt/ktouch/src/touch_listen.c b/opt/ktouch/src/touch_listen.c new file mode 100644 index 0000000..548160c --- /dev/null +++ b/opt/ktouch/src/touch_listen.c @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "touch_listen.h" + + +// 全局变量 +struct touch_device *devices = NULL; +int device_count = 0; +touch_event_callback event_callback = NULL; +static volatile int listener_running = 0; + + + + +// 计算字符串的简单哈希值,用于设备标识 +int hash_string(const char *str) { + int hash = 0; + while (*str) { + hash = hash * 31 + *str++; + } + return hash & 0x7FFFFFFF; // 确保是正数 +} + +// 检查设备是否是触摸屏 +bool is_touch_device(const char *device_path) { + int fd = open(device_path, O_RDONLY); + if (fd < 0) { + return false; + } + + unsigned long evbit = 0; + ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); + + // 检查设备是否支持绝对坐标和触摸事件 + if (!(evbit & (1 << EV_ABS)) || + !(evbit & (1 << EV_KEY))) { + close(fd); + return false; + } + + // 检查是否支持ABS_MT_POSITION_X事件(多点触控) + unsigned long absbit = 0; + ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absbit)), &absbit); + + if (absbit & (1 << ABS_MT_POSITION_X)) { + close(fd); + return true; + } + + // 检查是否支持ABS_X事件(单点触控) + if (absbit & (1 << ABS_X)) { + close(fd); + return true; + } + + close(fd); + return false; +} + +// 获取设备信息 +void get_device_info(struct touch_device *device) { + int fd = open(device->path, O_RDONLY); + if (fd < 0) { + return; + } + + // 获取设备名称 + ioctl(fd, EVIOCGNAME(sizeof(device->name)), device->name); + + // 获取X轴范围 + struct input_absinfo absinfo; + if (ioctl(fd, EVIOCGABS(ABS_X), &absinfo) >= 0) { + device->min_x = absinfo.minimum; + device->max_x = absinfo.maximum; + } + + // 获取Y轴范围 + if (ioctl(fd, EVIOCGABS(ABS_Y), &absinfo) >= 0) { + device->min_y = absinfo.minimum; + device->max_y = absinfo.maximum; + } + + close(fd); +} + +// 发现所有触摸屏设备 +int discover_touch_devices() { + DIR *dir; + struct dirent *entry; + char path[256]; + + // 释放之前分配的内存 + if (devices != NULL) { + for (int i = 0; i < device_count; i++) { + if (devices[i].fd >= 0) { + close(devices[i].fd); + } + } + free(devices); + devices = NULL; + device_count = 0; + } + + // 打开输入设备目录 + dir = opendir("/dev/input"); + if (!dir) { + perror("无法打开 /dev/input"); + return -1; + } + + // 第一次遍历,计算设备数量 + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "event", 5) == 0) { + snprintf(path, sizeof(path), "/dev/input/%s", entry->d_name); + if (is_touch_device(path)) { + device_count++; + } + } + } + + rewinddir(dir); + + // 分配设备数组内存 + devices = malloc(device_count * sizeof(struct touch_device)); + if (!devices) { + closedir(dir); + return -1; + } + + // 第二次遍历,填充设备信息 + int index = 0; + while ((entry = readdir(dir)) != NULL && index < device_count) { + if (strncmp(entry->d_name, "event", 5) == 0) { + snprintf(path, sizeof(path), "/dev/input/%s", entry->d_name); + if (is_touch_device(path)) { + strncpy(devices[index].path, path, sizeof(devices[index].path)); + get_device_info(&devices[index]); + devices[index].fd = -1; // 初始化为未打开状态 + index++; + } + } + } + + closedir(dir); + return device_count; +} + +// 打开所有触摸屏设备 +int open_touch_devices() { + for (int i = 0; i < device_count; i++) { + devices[i].fd = open(devices[i].path, O_RDONLY | O_NONBLOCK); + if (devices[i].fd < 0) { + perror("无法打开设备"); + return -1; + } + } + return 0; +} + +// 关闭所有触摸屏设备 +void close_touch_devices() { + for (int i = 0; i < device_count; i++) { + if (devices[i].fd >= 0) { + close(devices[i].fd); + devices[i].fd = -1; + } + } +} + +// 处理输入事件 +void process_input_event(struct input_event *ev, struct touch_device *device, + struct touch_event *touch_ev) { + static int x = 0, y = 0, pressure = 0, touch_id = 0; + + touch_ev->device_id = hash_string(device->path); + strncpy(touch_ev->device_name, device->name, sizeof(touch_ev->device_name)); + strncpy(touch_ev->device_path, device->path, sizeof(touch_ev->device_path)); + + // 获取当前时间戳 + clock_gettime(CLOCK_MONOTONIC, &touch_ev->timestamp); + + switch (ev->type) { + case EV_ABS: + switch (ev->code) { + case ABS_X: + case ABS_MT_POSITION_X: + x = ev->value; + // 转换为实际坐标(如果需要) + if (device->max_x > device->min_x) { + touch_ev->x = x; + } + break; + case ABS_Y: + case ABS_MT_POSITION_Y: + y = ev->value; + // 转换为实际坐标(如果需要) + if (device->max_y > device->min_y) { + touch_ev->y = y; + } + break; + case ABS_PRESSURE: + case ABS_MT_PRESSURE: + pressure = ev->value; + touch_ev->pressure = pressure; + break; + case ABS_MT_TRACKING_ID: + touch_id = ev->value; + touch_ev->touch_id = touch_id; + if (touch_id == -1) { + // 触摸释放 + touch_ev->event_type = 2; // 释放 + if (event_callback) { + event_callback(*touch_ev); + } + } else { + // 新触摸点 + touch_ev->event_type = 0; // 按下 + if (event_callback) { + event_callback(*touch_ev); + } + } + break; + } + break; + case EV_KEY: + if (ev->code == BTN_TOUCH) { + if (ev->value) { + touch_ev->event_type = 0; // 按下 + } else { + touch_ev->event_type = 2; // 释放 + } + if (event_callback) { + event_callback(*touch_ev); + } + } + break; + case EV_SYN: + if (ev->code == SYN_REPORT) { + // 报告同步事件,表示一组事件完成 + touch_ev->event_type = 1; // 移动 + touch_ev->x = x; + touch_ev->y = y; + touch_ev->pressure = pressure; + touch_ev->touch_id = touch_id; + if (event_callback && (x != 0 || y != 0)) { + event_callback(*touch_ev); + } + } + break; + } +} + +// 监听触摸事件 +void listen_touch_events() { + fd_set fds; + int max_fd = 0; + struct input_event ev; + struct touch_event touch_ev; + + listener_running = 1; // 设置运行标志 + + while (listener_running) { + FD_ZERO(&fds); + max_fd = 0; + + // 设置文件描述符集合 + for (int i = 0; i < device_count; i++) { + if (devices[i].fd >= 0) { + FD_SET(devices[i].fd, &fds); + if (devices[i].fd > max_fd) { + max_fd = devices[i].fd; + } + } + } + + if(listener_running == 0) break; + + if (max_fd == 0) { + usleep(100000); // 如果没有设备,短暂睡眠 + continue; + } + + if(listener_running == 0) break; + + // 使用select等待事件 + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + int ret = select(max_fd + 1, &fds, NULL, NULL, &timeout); + if (ret < 0) { + perror("select错误"); + break; + } + + if(listener_running == 0) break; + + // 检查每个设备是否有事件 + for (int i = 0; i < device_count; i++) { + if (devices[i].fd >= 0 && FD_ISSET(devices[i].fd, &fds)) { + // 读取所有可用事件 + while (read(devices[i].fd, &ev, sizeof(ev)) == sizeof(ev)) { + process_input_event(&ev, &devices[i], &touch_ev); + } + } + } + } + printf("listen_touch_events 成功退出\n"); +} + + +// 添加停止监听函数 +void stop_touch_listener() { + listener_running = 0; // 设置停止标志 +} + +// 格式化时间戳为可读字符串 +void format_timestamp(struct timespec ts, char *buffer, size_t buffer_size) { + time_t sec = ts.tv_sec; + struct tm *tm_info = localtime(&sec); + strftime(buffer, buffer_size, "%H:%M:%S", tm_info); + + // 添加毫秒部分 + char ms_buffer[10]; + snprintf(ms_buffer, sizeof(ms_buffer), ".%03ld", ts.tv_nsec / 1000000); + strncat(buffer, ms_buffer, buffer_size - strlen(buffer) - 1); +} + +// 示例回调函数 +void print_touch_event(struct touch_event event) { + const char *event_types[] = {"按下", "移动", "释放"}; + char timestamp[32]; + + format_timestamp(event.timestamp, timestamp, sizeof(timestamp)); + + printf("时间: %s\n", timestamp); + printf("设备: %s\n", event.device_name); + printf("路径: %s\n", event.device_path); + printf("设备ID: %d\n", event.device_id); + printf("事件: %s\n", event_types[event.event_type]); + printf("坐标: (%d, %d)\n", event.x, event.y); + printf("压力: %d\n", event.pressure); + if (event.touch_id >= 0) { + printf("触摸点ID: %d\n", event.touch_id); + } + printf("---\n"); +} + +// 初始化触摸事件监听 +int init_touch_listener(touch_event_callback callback) { + event_callback = callback; + + // 发现触摸设备 + if (discover_touch_devices() <= 0) { + fprintf(stderr, "未找到触摸屏设备\n"); + return -1; + } + + printf("找到 %d 个触摸屏设备:\n", device_count); + for (int i = 0; i < device_count; i++) { + printf("%d: %s (%s)\n", i, devices[i].name, devices[i].path); + } + + // 打开设备 + if (open_touch_devices() < 0) { + fprintf(stderr, "无法打开触摸设备\n"); + return -1; + } + + return 0; +} + +// 清理资源 +void cleanup_touch_listener() { + close_touch_devices(); + if (devices != NULL) { + free(devices); + devices = NULL; + } + device_count = 0; +} + +// 获取当前活动的触摸设备列表 +int get_active_touch_devices(struct touch_device **active_devices) { + if (device_count <= 0) { + return 0; + } + + *active_devices = malloc(device_count * sizeof(struct touch_device)); + if (!*active_devices) { + return -1; + } + + memcpy(*active_devices, devices, device_count * sizeof(struct touch_device)); + return device_count; +} + +// // 主函数示例 +// int main() { +// // 初始化触摸监听器 +// if (init_touch_listener(print_touch_event) < 0) { +// return 1; +// } + +// printf("开始监听触摸事件...\n"); +// printf("按Ctrl+C退出\n\n"); + +// // 开始监听事件 +// listen_touch_events(); + +// // 清理资源 +// cleanup_touch_listener(); + +// return 0; +// } \ No newline at end of file diff --git a/opt/ktouch/src/touch_listen.h b/opt/ktouch/src/touch_listen.h new file mode 100644 index 0000000..dbc31fc --- /dev/null +++ b/opt/ktouch/src/touch_listen.h @@ -0,0 +1,39 @@ + +#ifndef TOUCH_LISTEN +#define TOUCH_LISTEN + +// 触摸屏设备信息结构体 +struct touch_device { + int fd; + char name[256]; + char path[256]; + int min_x, max_x; + int min_y, max_y; +}; + +// 触摸事件信息结构体 +struct touch_event { + int device_id; // 设备标识(可以使用设备路径的哈希值) + int x; // X坐标 + int y; // Y坐标 + int pressure; // 压力值 + int touch_id; // 触摸点ID(用于多点触控) + int event_type; // 事件类型: 按下、移动、释放 + char device_name[256]; // 设备名称 + char device_path[256]; // 设备路径(如/dev/input/event1) + struct timespec timestamp; // 事件时间戳 +}; + + +// 回调函数类型定义 +typedef void (*touch_event_callback)(struct touch_event event); + +// 监听触摸事件 +void listen_touch_events(); + +// 初始化触摸事件监听 +int init_touch_listener(touch_event_callback callback); + +void stop_touch_listener(); + +#endif // !TOUCH_LISTEN \ No newline at end of file diff --git a/opt/ktouch/src/touch_set.c b/opt/ktouch/src/touch_set.c new file mode 100644 index 0000000..9bbbc09 --- /dev/null +++ b/opt/ktouch/src/touch_set.c @@ -0,0 +1,849 @@ +#include +#include +#include +#include +#include +#include // 为XA_STRING, XA_FLOAT等预定义原子 +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_LENGTH 256 +#define MAX_DEVICES 50 + +// 全局日志文件指针 +FILE *log_file = NULL; + +// 设备信息结构体 +typedef struct { + int device_id; // 设备ID + char name[256]; // 设备名称 + char device_node[256]; // 设备节点路径 + char vid[10]; // 供应商ID + char pid[10]; // 模型ID + char usb_path[100]; // 物理路径 + int is_touchscreen; // 是否是触摸屏设备标志 + float matrix[9]; + int matched; // 是否已匹配成功 +} InputDeviceInfo; + +// 配置结构体 +typedef struct { + char display_name[50]; + char touchscreen_name[50]; + char vid[10]; + char pid[10]; + char usb_path[100]; + int matched; // 是否已匹配成功 +} TouchConfig; + +// 屏幕信息结构体 +typedef struct { + char name[50]; + int width; + int height; + int x; + int y; + char rotation; // 旋转方向: N(正常), L(左转), R(右转), I(翻转) +} ScreenInfo; + + + +// 函数声明 +void log_message(const char* format, ...); +int read_config(const char* filename, TouchConfig configs[], int max_configs); +int read_screen_info(const char* filename, ScreenInfo screens[], int max_screens); +int find_matching_device(TouchConfig config, InputDeviceInfo devices[], int device_count, int strict); +int parse_resolution_and_position(const char* str, int* width, int* height, int* x, int* y); +void calculate_virtual_desktop(ScreenInfo screens[], int screen_count, int *total_width, int *total_height); +void calculate_ctm(ScreenInfo screen, int total_width, int total_height, float matrix[9]); +int set_ctm(int device_id, float matrix[9]); + + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] touch_set \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] touch_set \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + +// 将设备节点路径转换为 sysfs 路径 +char* devnode_to_syspath(const char* devnode) { + struct stat st; + if (stat(devnode, &st) != 0) { + return NULL; + } + + // 获取主设备号和次设备号 + unsigned int major = major(st.st_rdev); + unsigned int minor = minor(st.st_rdev); + + // 构建 sysfs 路径 + char* syspath = malloc(256); + snprintf(syspath, 256, "/sys/dev/char/%u:%u", major, minor); + + return syspath; +} + + + +// 获取输入设备信息的主要函数 +int get_input_devices(InputDeviceInfo result[]) { + Display *display; + int ndevices; + XIDeviceInfo *devices; + struct udev *udev_ctx; + int count = 0; + + // 打开X11显示连接 + display = XOpenDisplay(NULL); + if (!display) { + log_message("无法打开X显示连接\n"); + return 0; + } + + // 初始化udev上下文 + udev_ctx = udev_new(); + if (!udev_ctx) { + log_message("无法创建udev上下文\n"); + XCloseDisplay(display); + return 0; + } + + // 获取X输入设备列表 + devices = XIQueryDevice(display, XIAllDevices, &ndevices); + if (!devices) { + log_message( "无法查询X输入设备\n"); + udev_unref(udev_ctx); + XCloseDisplay(display); + return 0; + } + + + + // 遍历所有设备 + for (int i = 0; i < ndevices; i++) { + XIDeviceInfo *dev = &devices[i]; + + // 只关注从设备(slave devices),排除主设备和虚拟设备 + // if (dev->use != XISlavePointer && dev->use != XISlaveKeyboard) { + // continue; + // } + if(dev -> use != 3) continue; + + char *device_node = NULL; + + + // 获取设备属性 + int num_props = 0; + Atom *props = XIListProperties(display, dev->deviceid, &num_props); + // log_message("正在处理设备%d:%s(use=%d)\n", dev->deviceid, dev->name, dev->use); + if (props) { + for (int j = 0; j < num_props; j++) { + Atom prop = props[j]; + char *prop_name = XGetAtomName(display, prop); + + if (strcmp(prop_name, "Device Node") == 0) { + // 获取设备节点路径 + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *prop_data = NULL; + + if (XIGetProperty(display, dev->deviceid, prop, 0, 100, False, + AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &prop_data) == Success) { + // 使用 XInternAtom 获取字符串原子类型 + Atom string_atom = XInternAtom(display, "STRING", False); + if (actual_type == string_atom && actual_format == 8) { + device_node = strdup((char*)prop_data); + } + // log_message(" |- Device Node = %s\n", device_node); + XFree(prop_data); + } + } + + XFree(prop_name); + } + XFree(props); + } + + + + // 如果找到设备节点,通过udev查询更多信息 + char* syspath = devnode_to_syspath(device_node); + // log_message("Device syspath = %s\n", syspath); + + struct udev_device *udev_dev = udev_device_new_from_syspath(udev_ctx, syspath); + + + // const char *name = udev_device_get_property_value(udev_dev, "NAME"); + // 获取供应商ID + const char *vendor_id = udev_device_get_property_value(udev_dev, "ID_VENDOR_ID"); + // 获取模型ID + const char *model_id = udev_device_get_property_value(udev_dev, "ID_MODEL_ID"); + // 获取物理路径 + const char *physical_path = udev_device_get_property_value(udev_dev, "ID_PATH"); + // 检查是否是触摸屏设备 + const char *is_touchscreen = udev_device_get_property_value(udev_dev, "ID_INPUT_TOUCHSCREEN"); + + // log_message(" |- Device info: vid=%s, pid=%s, path=%s, touch=%s\n", vendor_id, model_id, physical_path, is_touchscreen); + + + + + + if (is_touchscreen){ + // log_message("识别到触摸设备,添加到列表中\n name=%s", name); + // 初始化当前设备信息结构体 + result[count].device_id = dev->deviceid; + result[count].matched = 0; + strncpy(result[count].name, dev->name, sizeof(result[count].name)); + strncpy(result[count].vid, vendor_id, sizeof(result[count].vid)); + strncpy(result[count].pid, model_id, sizeof(result[count].pid)); + strncpy(result[count].usb_path, physical_path, sizeof(result[count].usb_path)); + strncpy(result[count].device_node, device_node, sizeof(result[count].device_node)); + for(int i=0;i<9;i++){ + result[count].matrix[i] = 0; + } + // result[count].device_node = NULL; + // result[count].name = strdup(dev->name); + // result[count].device_node = strdup(device_node); + // result[count].vid = strdup(vendor_id); + // result[count].pid = strdup(model_id); + // result[count].usb_path = strdup(physical_path); + result[count].is_touchscreen = 1; + log_message("发现触摸屏设备 %s(id=%d), %s:%s, path=%s\n",result[count].name, result[count].device_id, + result[count].vid, result[count].pid,result[count].usb_path); + + count++; + + } + udev_device_unref(udev_dev); + + + } + + // 释放资源 + XIFreeDeviceInfo(devices); + udev_unref(udev_ctx); + XCloseDisplay(display); + + return count; +} + +// 读取配置文件 +int read_config(const char* filename, TouchConfig configs[], int max_configs) { + FILE* file = fopen(filename, "r"); + if (!file) { + log_message("无法打开配置文件: %s\n", filename); + return -1; + } + + char line[MAX_LINE_LENGTH]; + int count = 0; + + while (fgets(line, sizeof(line), file) && count < max_configs) { + // 移除换行符 + line[strcspn(line, "\n")] = 0; + + // 跳过空行和注释 + if (line[0] == '\0' || line[0] == '#') continue; + + // 解析行 + char* tokens[5]; + char* token = strtok(line, "|"); + int i = 0; + + while (token && i < 5) { + tokens[i++] = token; + token = strtok(NULL, "|"); + } + + if (i == 5) { + strncpy(configs[count].display_name, tokens[0], sizeof(configs[count].display_name)); + strncpy(configs[count].touchscreen_name, tokens[1], sizeof(configs[count].touchscreen_name)); + strncpy(configs[count].vid, tokens[2], sizeof(configs[count].vid)); + strncpy(configs[count].pid, tokens[3], sizeof(configs[count].pid)); + strncpy(configs[count].usb_path, tokens[4], sizeof(configs[count].usb_path)); + configs[count].matched = 0; + count++; + } else { + log_message("无效的配置行: %s\n", line); + } + } + + fclose(file); + return count; +} + +// 读取屏幕信息 +int read_screen_info(const char* filename, ScreenInfo screens[], int max_screens) { + FILE* file = fopen(filename, "r"); + if (!file) { + log_message("无法打开屏幕信息文件: %s\n", filename); + return -1; + } + + char line[MAX_LINE_LENGTH]; + int count = 0; + + while (fgets(line, sizeof(line), file) && count < max_screens) { + // 移除换行符 + line[strcspn(line, "\n")] = 0; + + // 跳过空行和注释 + if (line[0] == '\0' || line[0] == '#') continue; + + // 解析显示器名 + char* name = strtok(line, "|"); + if (!name) continue; + + // 解析分辨率 + char* resolution = strtok(NULL, "|"); + if (!resolution) continue; + + // 解析坐标 + char* position = strtok(NULL, "|"); + if (!position) continue; + + // 解析旋转方向(可选,默认为N) + char* rotation_str = strtok(NULL, "|"); + char rotation = 'N'; // 默认正常方向 + + if (rotation_str) { + rotation = toupper(rotation_str[0]); + if (rotation != 'N' && rotation != 'L' && rotation != 'R' && rotation != 'I') { + log_message("无效的旋转方向: %s,使用默认值N\n", rotation_str); + rotation = 'N'; + } + } + + int width, height, x, y; + // 解析分辨率 (格式: 1920x1080) + if (sscanf(resolution, "%dx%d", &width, &height) != 2) { + log_message("无效的分辨率格式: %s\n", resolution); + continue; + } + + // 解析坐标 (格式: 1920x0 或 1920,0) + if (sscanf(position, "%d%*[x,]%d", &x, &y) != 2) { + log_message("无效的坐标格式: %s\n", position); + continue; + } + + strncpy(screens[count].name, name, sizeof(screens[count].name)); + screens[count].width = width; + screens[count].height = height; + screens[count].x = x; + screens[count].y = y; + screens[count].rotation = rotation; + count++; + + log_message("屏幕 %s: 分辨率 %dx%d, 位置 (%d,%d), 旋转方向 %c\n", + name, width, height, x, y, rotation); + } + + fclose(file); + return count; +} + +// 辅助函数:去除字符串中的空格 +void remove_spaces(char* str) { + char* i = str; + char* j = str; + while (*j != '\0') { + *i = *j++; + if (*i != ' ') { + i++; + } + } + *i = '\0'; +} + + +// 查找匹配的设备 +int find_matching_device(TouchConfig config, InputDeviceInfo devices[], int device_count, int strict) { + int match_index = -1; + int match_count = 0; + int match_indices[MAX_DEVICES]; + + log_message("查找匹配设备: %s (VID:%s PID:%s 路径:%s)\n", + config.touchscreen_name, config.vid, config.pid, config.usb_path); + + // 第一步:按设备名完全匹配 + for (int i = 0; i < device_count; i++) { + // 跳过已匹配的设备 + if (devices[i].matched) { + continue; + } + + char device_name_no_space[256]; + char config_name_no_space[256]; + + strcpy(device_name_no_space, devices[i].name); + strcpy(config_name_no_space, config.touchscreen_name); + + remove_spaces(device_name_no_space); + remove_spaces(config_name_no_space); + + if (strcmp(device_name_no_space, config_name_no_space) == 0) { + match_indices[match_count++] = i; + log_message("名称完全匹配(去除空格): %s -> %s\n", devices[i].name, device_name_no_space); + } + } + + // 如果只有一个匹配项,直接返回 + if (match_count == 1) { + return match_indices[0]; + } + + // 如果有多个匹配项,按vid:pid进一步匹配 // 严格模式 + if (match_count > 1 && strict) { + // match_count = 0; + log_message("找到多个名称匹配项,按VID:PID进一步筛选\n"); + int vid_pid_match_count = 0; + int vid_pid_match_indices[MAX_DEVICES]; + + for (int i = 0; i < match_count; i++) { + int idx = match_indices[i]; + if (strcasecmp(devices[idx].vid, config.vid) == 0 && + strcasecmp(devices[idx].pid, config.pid) == 0) { + vid_pid_match_indices[vid_pid_match_count++] = idx; + log_message("VID:PID匹配: %s (VID:%s PID:%s)\n", + devices[idx].name, devices[idx].vid, devices[idx].pid); + } + } + + if (vid_pid_match_count == 1) { + return vid_pid_match_indices[0]; + } + + // 严格模式下,多个pid、vid匹配,按usb路径匹配 + if (vid_pid_match_count > 1 && config.usb_path[0] != '\0') { + log_message("找到多个VID:PID匹配项,按USB路径进一步筛选\n"); + for (int i = 0; i < vid_pid_match_count; i++) { + int idx = vid_pid_match_indices[i]; + if (strcmp(devices[idx].usb_path, config.usb_path) == 0) { + log_message("USB路径匹配: %s\n", devices[idx].usb_path); + return idx; // 返回第一个匹配的 + } + } + } + + // 如果还是无法确定,则失败 + + log_message("都无法匹配,返回失败:-1\n"); + return -1; + + } + + // 宽松匹配:要求名称和id都匹配的上 + // 程序能跑到这里,说明有多个名称匹配了 + if (!strict) { + log_message("未找到名称匹配项,尝试VID:PID匹配\n"); + for (int i = 0; i < device_count; i++) { + // 跳过已匹配的设备 + if (devices[i].matched) { + continue; + } + + if (strcasecmp(devices[i].vid, config.vid) == 0 && + strcasecmp(devices[i].pid, config.pid) == 0) { + match_indices[match_count++] = i; + log_message("VID:PID匹配: %s (VID:%s PID:%s)\n", + devices[i].name, devices[i].vid, devices[i].pid); + } + } + + if (match_count == 1) { + return match_indices[0]; + } + + + // 如果还是无法确定,返回第一个匹配项 + if (match_count > 0) { + log_message("仍有多个匹配项,返回失败\n"); + return -1; + } + } + + log_message("未找到匹配设备\n"); + return -1; +} + + +/* ================================== + + = 矩阵坐标计算相关代码 = + +======================================*/ + +// 计算虚拟桌面大小 +void calculate_virtual_desktop(ScreenInfo screens[], int screen_count, int *total_width, int *total_height) { + int min_x = 0, min_y = 0; + int max_x = 0, max_y = 0; + + for (int i = 0; i < screen_count; i++) { + int screen_right = screens[i].x + screens[i].width; + int screen_bottom = screens[i].y + screens[i].height; + + if (screens[i].x < min_x) min_x = screens[i].x; + if (screens[i].y < min_y) min_y = screens[i].y; + if (screen_right > max_x) max_x = screen_right; + if (screen_bottom > max_y) max_y = screen_bottom; + } + + *total_width = max_x - min_x; + *total_height = max_y - min_y; + + log_message("虚拟桌面边界: x(%d 到 %d), y(%d 到 %d)\n", + min_x, max_x, min_y, max_y); +} + +// 计算坐标转换矩阵的函数(考虑旋转方向) +void calculate_ctm(ScreenInfo screen, int total_width, int total_height, float matrix[9]) { + + /* + * 屏幕旋转的计算 + * + * 在逻辑处理中,系统需要将屏幕按相同方向旋转(逆时针为正向) + * 然后将图形向左平移回正坐标 + * 最后进行传统缩放。 + * + * 旋转矩阵 + * cosθ -sinθ 0 + * sinθ cosθ 0 + * 0 0 1 + * + * “向左旋转”后,屏幕需要逆时针旋转90度才能正确显示,同样的系统也是逆时针90度。 + * 由此可以推算出90、180、270 三个旋转的矩阵 + * + */ + + + float sx = (float)screen.width / total_width; + float sy = (float)screen.height / total_height; + float dx = (float)screen.x / total_width; + float dy = (float)screen.y / total_height; + + + // 根据旋转方向调整矩阵 + switch (screen.rotation) { + case 'L': // 左转90度 + matrix[0] = 0; matrix[1] = -sx; matrix[2] = sx+dx; + matrix[3] = sy; matrix[4] = 0.0f; matrix[5] = dy; + matrix[6] = 0.0f; matrix[7] = 0.0f; matrix[8] = 1.0f; + break; + + + case 'R': // 右转90 + matrix[0] = 0.0f; matrix[1] = sx; matrix[2] = dx; + matrix[3] = -sy; matrix[4] = 0.0f; matrix[5] = sy + dy; + matrix[6] = 0.0f; matrix[7] = 0.0f; matrix[8] = 1.0f; + break; + + + case 'I': // 翻转180度 + matrix[0] = -sx; matrix[1] = 0.0f; matrix[2] = sx + dx; + matrix[3] = 0.0f; matrix[4] = -sy; matrix[5] = sy + dy; + matrix[6] = 0.0f; matrix[7] = 0.0f; matrix[8] = 1.0f; + break; + + + case 'N': // 正常方向 + default: + matrix[0] = sx; matrix[1] = 0.0f; matrix[2] = dx; + matrix[3] = 0.0f; matrix[4] = sy; matrix[5] = dy; + matrix[6] = 0.0f; matrix[7] = 0.0f; matrix[8] = 1.0f; + break; + } + + + +} + +// 设置设备的坐标转换矩阵 +int set_ctm(int device_id, float matrix[9]) { + Display *display = XOpenDisplay(NULL); + if (!display) { + fprintf(stderr, "无法打开X显示连接\n"); + return 0; + } + + // 创建ATOM属性 + Atom prop = XInternAtom(display, "Coordinate Transformation Matrix", False); + if (!prop) { + fprintf(stderr, "无法创建Coordinate Transformation Matrix属性\n"); + XCloseDisplay(display); + return 0; + } + + // 检查设备是否支持此属性 + int num_props = 0; + Atom *props = XIListProperties(display, device_id, &num_props); + int supports_matrix = 0; + + if (props) { + for (int i = 0; i < num_props; i++) { + char *prop_name = XGetAtomName(display, props[i]); + if (strcmp(prop_name, "Coordinate Transformation Matrix") == 0) { + supports_matrix = 1; + XFree(prop_name); + break; + } + XFree(prop_name); + } + XFree(props); + } + + if (!supports_matrix) { + fprintf(stderr, "设备 %d 不支持坐标转换矩阵属性\n", device_id); + XCloseDisplay(display); + return 0; + } + + // 创建FLOAT类型的原子 + Atom float_atom = XInternAtom(display, "FLOAT", False); + if (!float_atom) { + fprintf(stderr, "无法创建FLOAT类型原子\n"); + XCloseDisplay(display); + return 0; + } + + // 设置属性值 + XIChangeProperty(display, device_id, prop, float_atom, 32, PropModeReplace, + (unsigned char*)matrix, 9); + + XFlush(display); + XCloseDisplay(display); + return 1; +} + +// 示例使用 +int main(int argc, char *argv[]) { + char *output_file_path = NULL; + + // 处理命令行参数 + if (argc > 1) { + output_file_path = argv[1]; + log_message("输出文件路径: %s\n", output_file_path); + } + + // 打开日志文件 + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("警告: 无法打开日志文件 /opt/ktouch/sub_modules.log,仅输出到控制台\n"); + } + + log_message("开始触摸屏配置...\n"); + + TouchConfig configs[MAX_DEVICES]; + ScreenInfo screens[MAX_DEVICES]; + InputDeviceInfo devices[MAX_DEVICES]; + + int config_count = read_config("/opt/ktouch/config", configs, MAX_DEVICES); + if (config_count < 0) { + log_message("读取配置文件失败\n"); + if (log_file) fclose(log_file); + return 1; + } + log_message("读取了 %d 个配置项\n", config_count); + + int screen_count = read_screen_info("/tmp/ktouch/screen.txt", screens, MAX_DEVICES); + if (screen_count < 0) { + log_message("读取屏幕信息文件失败\n"); + if (log_file) fclose(log_file); + return 1; + } + log_message("读取了 %d 个屏幕信息\n", screen_count); + + // 计算虚拟桌面大小 + int total_width, total_height; + calculate_virtual_desktop(screens, screen_count, &total_width, &total_height); + log_message("虚拟桌面大小: %dx%d\n", total_width, total_height); + + int device_count = 0; + device_count = get_input_devices(devices); + log_message("找到了 %d 个触摸设备\n", device_count); + + // 输出所有找到的设备信息 + for (int i = 0; i < device_count; i++) { + log_message("设备 %d: %s (VID:%s PID:%s 路径:%s XInput ID:%d)\n", + i, devices[i].name, devices[i].vid, devices[i].pid, + devices[i].usb_path, devices[i].device_id); + } + + // 多轮匹配:首先严格匹配,然后宽松匹配,直到无法匹配 + int round = 1; + int matched_in_round = 1; // 初始化为1以进入循环 + + while (matched_in_round > 0) { + matched_in_round = 0; + log_message("开始第 %d 轮匹配\n", round); + + // 确定匹配严格程度 + int strict = (round == 1); // 第一轮严格匹配,后续宽松匹配 + + // 对每个未匹配的配置项查找匹配的设备 + for (int i = 0; i < config_count; i++) { + if (configs[i].matched) { + continue; // 跳过已匹配的配置项 + } + + log_message("处理配置: %s|%s|%s|%s|%s\n", + configs[i].display_name, configs[i].touchscreen_name, + configs[i].vid, configs[i].pid, configs[i].usb_path); + + int device_index = find_matching_device(configs[i], devices, device_count, strict); + if (device_index == -1) { + continue; // 未找到匹配设备,继续下一个配置项 + } + + log_message("匹配设备: %s (VID:%s PID:%s 路径:%s XInput ID:%d)\n", + devices[device_index].name, devices[device_index].vid, + devices[device_index].pid, devices[device_index].usb_path, + devices[device_index].device_id); + + // 标记设备和配置项为已匹配 + devices[device_index].matched = 1; + configs[i].matched = 1; + matched_in_round++; + + // 检查XInput设备ID是否有效 + if (devices[device_index].device_id == -1) { + log_message("错误: 设备 %s 没有有效的XInput ID\n", devices[device_index].name); + continue; + } + + // 查找对应的屏幕信息 + ScreenInfo* matched_screen = NULL; + for (int j = 0; j < screen_count; j++) { + if (strcmp(screens[j].name, configs[i].display_name) == 0) { + matched_screen = &screens[j]; + break; + } + } + + if (matched_screen == NULL) { + log_message("未找到 %s 对应的屏幕, 将使用第一屏\n", configs[i].display_name); + // 未找到屏幕,则返回第一个屏幕 + matched_screen = &screens[0]; + } + + log_message("匹配屏幕: %s %dx%d 位置(%d,%d) 旋转方向 %c\n", + matched_screen->name, matched_screen->width, + matched_screen->height, matched_screen->x, matched_screen->y, + matched_screen->rotation); + + // 计算并设置矩阵 + float matrix[9]={0}; + calculate_ctm(*matched_screen, total_width, total_height, matrix); + + log_message("计算矩阵: [%f, %f, %f, %f, %f, %f, %f, %f, %f]\n", + matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5], + matrix[6], matrix[7], matrix[8]); + + set_ctm(devices[device_index].device_id, matrix); + memcpy(devices[device_index].matrix, matrix, sizeof(float) * 9); + + log_message("已配置 %s 为 %s 的触摸屏\n", + devices[device_index].name, configs[i].display_name); + } + + log_message("第 %d 轮匹配完成,匹配了 %d 个设备\n", round, matched_in_round); + round++; + } + + // 保存到文件 + if (output_file_path) { + int max_retries = 3; + int retry_delay = 1; // 秒 + FILE *output_file = NULL; + + for (int retry = 0; retry < max_retries; retry++) { + output_file = fopen(output_file_path, "w"); + if (output_file) { + break; + } + log_message("无法打开输出文件 %s (尝试 %d/%d),%d秒后重试...\n", + output_file_path, retry + 1, max_retries, retry_delay); + sleep(retry_delay); + } + + if (output_file) { + int wcount = 0; + for (int i = 0; i < device_count; i++) { + if(devices[i].matched == 0){ + continue; + } + wcount++; + fprintf(output_file, "%d|%s|%s|%s:%s|%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f\n", + devices[i].device_id, + devices[i].name, + devices[i].device_node, + devices[i].vid, + devices[i].pid, + devices[i].matrix[0], devices[i].matrix[1], devices[i].matrix[2], + devices[i].matrix[3], devices[i].matrix[4], devices[i].matrix[5], + devices[i].matrix[6], devices[i].matrix[7], devices[i].matrix[8]); + } + fclose(output_file); + log_message("已成功写入 %d 条记录到文件 %s\n", wcount, output_file_path); + } else { + log_message("经过 %d 次尝试后仍无法打开输出文件 %s,放弃写入\n", + max_retries, output_file_path); + } + } + + // 输出未匹配的设备信息 + int unmatched_devices = 0; + for (int i = 0; i < device_count; i++) { + if (!devices[i].matched) { + unmatched_devices++; + log_message("警告: 设备 %s (VID:%s PID:%s) 未匹配到任何配置\n", + devices[i].name, devices[i].vid, devices[i].pid); + } + } + + // 输出未匹配的配置信息 + int unmatched_configs = 0; + for (int i = 0; i < config_count; i++) { + if (!configs[i].matched) { + unmatched_configs++; + log_message("警告: 配置 %s 未匹配到任何设备\n", configs[i].touchscreen_name); + } + } + + log_message("触摸屏配置完成,%d 个设备未匹配,%d 个配置未匹配\n", + unmatched_devices, unmatched_configs); + + if (log_file) { + fclose(log_file); + } + + return 0; + +} diff --git a/opt/ktouch/src/touch_set.workfine.c b/opt/ktouch/src/touch_set.workfine.c new file mode 100644 index 0000000..da17bc4 --- /dev/null +++ b/opt/ktouch/src/touch_set.workfine.c @@ -0,0 +1,754 @@ +#include +#include +#include +#include +#include +#include // 为XA_STRING, XA_FLOAT等预定义原子 +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_LENGTH 256 +#define MAX_DEVICES 50 + +// 全局日志文件指针 +FILE *log_file = NULL; + +// 设备信息结构体 +typedef struct { + int device_id; // 设备ID + char name[256]; // 设备名称 + char device_node[256]; // 设备节点路径 + char vid[10]; // 供应商ID + char pid[10]; // 模型ID + char usb_path[100]; // 物理路径 + int is_touchscreen; // 是否是触摸屏设备标志 + float matrix[9]; + int match_success; +} InputDeviceInfo; + +// 配置结构体 +typedef struct { + char display_name[50]; + char touchscreen_name[50]; + char vid[10]; + char pid[10]; + char usb_path[100]; +} TouchConfig; + +// 屏幕信息结构体 +typedef struct { + char name[50]; + int width; + int height; + int x; + int y; +} ScreenInfo; + + + +// 函数声明 +void log_message(const char* format, ...); +int read_config(const char* filename, TouchConfig configs[], int max_configs); +int read_screen_info(const char* filename, ScreenInfo screens[], int max_screens); +int find_matching_device(TouchConfig config, InputDeviceInfo devices[], int device_count); +int parse_resolution_and_position(const char* str, int* width, int* height, int* x, int* y); +void calculate_virtual_desktop(ScreenInfo screens[], int screen_count, int *total_width, int *total_height); +void calculate_ctm(ScreenInfo screen, int total_width, int total_height, float matrix[9]); +int set_ctm(int device_id, float matrix[9]); + + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] touch_set \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] touch_set \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + +// 将设备节点路径转换为 sysfs 路径 +char* devnode_to_syspath(const char* devnode) { + struct stat st; + if (stat(devnode, &st) != 0) { + return NULL; + } + + // 获取主设备号和次设备号 + unsigned int major = major(st.st_rdev); + unsigned int minor = minor(st.st_rdev); + + // 构建 sysfs 路径 + char* syspath = malloc(256); + snprintf(syspath, 256, "/sys/dev/char/%u:%u", major, minor); + + return syspath; +} + + + +// 获取输入设备信息的主要函数 +int get_input_devices(InputDeviceInfo result[]) { + Display *display; + int ndevices; + XIDeviceInfo *devices; + struct udev *udev_ctx; + int count = 0; + + // 打开X11显示连接 + display = XOpenDisplay(NULL); + if (!display) { + log_message("无法打开X显示连接\n"); + return 0; + } + + // 初始化udev上下文 + udev_ctx = udev_new(); + if (!udev_ctx) { + log_message("无法创建udev上下文\n"); + XCloseDisplay(display); + return 0; + } + + // 获取X输入设备列表 + devices = XIQueryDevice(display, XIAllDevices, &ndevices); + if (!devices) { + log_message( "无法查询X输入设备\n"); + udev_unref(udev_ctx); + XCloseDisplay(display); + return 0; + } + + + + // 遍历所有设备 + for (int i = 0; i < ndevices; i++) { + XIDeviceInfo *dev = &devices[i]; + + // 只关注从设备(slave devices),排除主设备和虚拟设备 + // if (dev->use != XISlavePointer && dev->use != XISlaveKeyboard) { + // continue; + // } + if(dev -> use != 3) continue; + + char *device_node = NULL; + + + // 获取设备属性 + int num_props = 0; + Atom *props = XIListProperties(display, dev->deviceid, &num_props); + // log_message("正在处理设备%d:%s(use=%d)\n", dev->deviceid, dev->name, dev->use); + if (props) { + for (int j = 0; j < num_props; j++) { + Atom prop = props[j]; + char *prop_name = XGetAtomName(display, prop); + + if (strcmp(prop_name, "Device Node") == 0) { + // 获取设备节点路径 + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *prop_data = NULL; + + if (XIGetProperty(display, dev->deviceid, prop, 0, 100, False, + AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &prop_data) == Success) { + // 使用 XInternAtom 获取字符串原子类型 + Atom string_atom = XInternAtom(display, "STRING", False); + if (actual_type == string_atom && actual_format == 8) { + device_node = strdup((char*)prop_data); + } + // log_message(" |- Device Node = %s\n", device_node); + XFree(prop_data); + } + } + + XFree(prop_name); + } + XFree(props); + } + + + + // 如果找到设备节点,通过udev查询更多信息 + char* syspath = devnode_to_syspath(device_node); + // log_message("Device syspath = %s\n", syspath); + + struct udev_device *udev_dev = udev_device_new_from_syspath(udev_ctx, syspath); + + + // const char *name = udev_device_get_property_value(udev_dev, "NAME"); + // 获取供应商ID + const char *vendor_id = udev_device_get_property_value(udev_dev, "ID_VENDOR_ID"); + // 获取模型ID + const char *model_id = udev_device_get_property_value(udev_dev, "ID_MODEL_ID"); + // 获取物理路径 + const char *physical_path = udev_device_get_property_value(udev_dev, "ID_PATH"); + // 检查是否是触摸屏设备 + const char *is_touchscreen = udev_device_get_property_value(udev_dev, "ID_INPUT_TOUCHSCREEN"); + + // log_message(" |- Device info: vid=%s, pid=%s, path=%s, touch=%s\n", vendor_id, model_id, physical_path, is_touchscreen); + + + + + + if (is_touchscreen){ + // log_message("识别到触摸设备,添加到列表中\n name=%s", name); + // 初始化当前设备信息结构体 + result[count].device_id = dev->deviceid; + result[count].match_success = 0; + strncpy(result[count].name, dev->name, sizeof(result[count].name)); + strncpy(result[count].vid, vendor_id, sizeof(result[count].vid)); + strncpy(result[count].pid, model_id, sizeof(result[count].pid)); + strncpy(result[count].usb_path, physical_path, sizeof(result[count].usb_path)); + strncpy(result[count].device_node, device_node, sizeof(result[count].device_node)); + for(int i=0;i<9;i++){ + result[count].matrix[i] = 0; + } + // result[count].device_node = NULL; + // result[count].name = strdup(dev->name); + // result[count].device_node = strdup(device_node); + // result[count].vid = strdup(vendor_id); + // result[count].pid = strdup(model_id); + // result[count].usb_path = strdup(physical_path); + result[count].is_touchscreen = 1; + log_message("发现触摸屏设备 %s(id=%d), %s:%s, path=%s\n",result[count].name, result[count].device_id, + result[count].vid, result[count].pid,result[count].usb_path); + + count++; + + } + udev_device_unref(udev_dev); + + + } + + // 释放资源 + XIFreeDeviceInfo(devices); + udev_unref(udev_ctx); + XCloseDisplay(display); + + return count; +} + +// 读取配置文件 +int read_config(const char* filename, TouchConfig configs[], int max_configs) { + FILE* file = fopen(filename, "r"); + if (!file) { + log_message("无法打开配置文件: %s\n", filename); + return -1; + } + + char line[MAX_LINE_LENGTH]; + int count = 0; + + while (fgets(line, sizeof(line), file) && count < max_configs) { + // 移除换行符 + line[strcspn(line, "\n")] = 0; + + // 跳过空行和注释 + if (line[0] == '\0' || line[0] == '#') continue; + + // 解析行 + char* tokens[5]; + char* token = strtok(line, "|"); + int i = 0; + + while (token && i < 5) { + tokens[i++] = token; + token = strtok(NULL, "|"); + } + + if (i == 5) { + strncpy(configs[count].display_name, tokens[0], sizeof(configs[count].display_name)); + strncpy(configs[count].touchscreen_name, tokens[1], sizeof(configs[count].touchscreen_name)); + strncpy(configs[count].vid, tokens[2], sizeof(configs[count].vid)); + strncpy(configs[count].pid, tokens[3], sizeof(configs[count].pid)); + strncpy(configs[count].usb_path, tokens[4], sizeof(configs[count].usb_path)); + count++; + } else { + log_message("无效的配置行: %s\n", line); + } + } + + fclose(file); + return count; +} + +// 读取屏幕信息 +int read_screen_info(const char* filename, ScreenInfo screens[], int max_screens) { + FILE* file = fopen(filename, "r"); + if (!file) { + log_message("无法打开屏幕信息文件: %s\n", filename); + return -1; + } + + char line[MAX_LINE_LENGTH]; + int count = 0; + + while (fgets(line, sizeof(line), file) && count < max_screens) { + // 移除换行符 + line[strcspn(line, "\n")] = 0; + + // 跳过空行和注释 + if (line[0] == '\0' || line[0] == '#') continue; + + // 解析显示器名 + char* name = strtok(line, "|"); + if (!name) continue; + + // 解析分辨率 + char* resolution = strtok(NULL, "|"); + if (!resolution) continue; + + // 解析坐标 + char* position = strtok(NULL, "|"); + if (!position) continue; + + int width, height, x, y; + // 解析分辨率 (格式: 1920x1080) + if (sscanf(resolution, "%dx%d", &width, &height) != 2) { + log_message("无效的分辨率格式: %s\n", resolution); + continue; + } + + // 解析坐标 (格式: 1920x0 或 1920,0) + if (sscanf(position, "%d%*[x,]%d", &x, &y) != 2) { + log_message("无效的坐标格式: %s\n", position); + continue; + } + + strncpy(screens[count].name, name, sizeof(screens[count].name)); + screens[count].width = width; + screens[count].height = height; + screens[count].x = x; + screens[count].y = y; + count++; + + log_message("屏幕 %s: 分辨率 %dx%d, 位置 (%d,%d)\n", name, width, height, x, y); + } + + fclose(file); + return count; +} + +// 辅助函数:去除字符串中的空格 +void remove_spaces(char* str) { + char* i = str; + char* j = str; + while (*j != '\0') { + *i = *j++; + if (*i != ' ') { + i++; + } + } + *i = '\0'; +} + + +// 查找匹配的设备 +int find_matching_device(TouchConfig config, InputDeviceInfo devices[], int device_count) { + int match_index = -1; + int match_count = 0; + int match_indices[MAX_DEVICES]; + + log_message("查找匹配设备: %s (VID:%s PID:%s 路径:%s)\n", + config.touchscreen_name, config.vid, config.pid, config.usb_path); + + // 第一步:按设备名完全匹配 + for (int i = 0; i < device_count; i++) { + // if (strcmp(devices[i].name, config.touchscreen_name) == 0) { + // match_indices[match_count++] = i; + // log_message("名称完全匹配: %s\n", devices[i].name); + // } + + char device_name_no_space[256]; + char config_name_no_space[256]; + + strcpy(device_name_no_space, devices[i].name); + strcpy(config_name_no_space, config.touchscreen_name); + + remove_spaces(device_name_no_space); + remove_spaces(config_name_no_space); + + if (strcmp(device_name_no_space, config_name_no_space) == 0) { + match_indices[match_count++] = i; + log_message("名称完全匹配(去除空格): %s -> %s\n", devices[i].name, device_name_no_space); + } + } + + // 如果只有一个匹配项,直接返回 + if (match_count == 1) { + return match_indices[0]; + } + + // 如果有多个匹配项,按vid:pid进一步匹配 + if (match_count > 1) { + log_message("找到多个名称匹配项,按VID:PID进一步筛选\n"); + int vid_pid_match_count = 0; + int vid_pid_match_indices[MAX_DEVICES]; + + for (int i = 0; i < match_count; i++) { + int idx = match_indices[i]; + if (strcasecmp(devices[idx].vid, config.vid) == 0 && + strcasecmp(devices[idx].pid, config.pid) == 0) { + vid_pid_match_indices[vid_pid_match_count++] = idx; + log_message("VID:PID匹配: %s (VID:%s PID:%s)\n", + devices[idx].name, devices[idx].vid, devices[idx].pid); + } + } + + if (vid_pid_match_count == 1) { + return vid_pid_match_indices[0]; + } + + // 如果还有多个匹配项,按usb路径匹配 + if (vid_pid_match_count > 1 && config.usb_path[0] != '\0') { + log_message("找到多个VID:PID匹配项,按USB路径进一步筛选\n"); + for (int i = 0; i < vid_pid_match_count; i++) { + int idx = vid_pid_match_indices[i]; + if (strcmp(devices[idx].usb_path, config.usb_path) == 0) { + log_message("USB路径匹配: %s\n", devices[idx].usb_path); + return idx; // 返回第一个匹配的 + } + } + } + + // 如果还是无法确定,返回第一个匹配项 + if (vid_pid_match_count > 0) { + log_message("仍有多个匹配项,返回第一个\n"); + return vid_pid_match_indices[0]; + } + } + + // // 如果没有名称匹配项,尝试按vid:pid匹配 + // log_message("未找到名称匹配项,尝试VID:PID匹配\n"); + // for (int i = 0; i < device_count; i++) { + // if (strcasecmp(devices[i].vid, config.vid) == 0 && + // strcasecmp(devices[i].pid, config.pid) == 0) { + // match_indices[match_count++] = i; + // log_message("VID:PID匹配: %s (VID:%s PID:%s)\n", + // devices[i].name, devices[i].vid, devices[i].pid); + // } + // } + + // if (match_count == 1) { + // return match_indices[0]; + // } + + // // 如果还有多个匹配项,按usb路径匹配 + // if (match_count > 1 && config.usb_path[0] != '\0') { + // log_message("找到多个VID:PID匹配项,按USB路径进一步筛选\n"); + // for (int i = 0; i < match_count; i++) { + // int idx = match_indices[i]; + // if (strcmp(devices[idx].usb_path, config.usb_path) == 0) { + // log_message("USB路径匹配: %s\n", devices[idx].usb_path); + // return idx; + // } + // } + // } + + // // 如果还是无法确定,返回第一个匹配项或-1 + // if (match_count > 0) { + // log_message("仍有多个匹配项,返回第一个\n"); + // return match_indices[0]; + // } + + log_message("未找到匹配设备\n"); + return -1; +} + + +/* ================================== + + = 矩阵坐标计算相关代码 = + +======================================*/ + +// 计算虚拟桌面大小 +void calculate_virtual_desktop(ScreenInfo screens[], int screen_count, int *total_width, int *total_height) { + int min_x = 0, min_y = 0; + int max_x = 0, max_y = 0; + + for (int i = 0; i < screen_count; i++) { + int screen_right = screens[i].x + screens[i].width; + int screen_bottom = screens[i].y + screens[i].height; + + if (screens[i].x < min_x) min_x = screens[i].x; + if (screens[i].y < min_y) min_y = screens[i].y; + if (screen_right > max_x) max_x = screen_right; + if (screen_bottom > max_y) max_y = screen_bottom; + } + + *total_width = max_x - min_x; + *total_height = max_y - min_y; + + log_message("虚拟桌面边界: x(%d 到 %d), y(%d 到 %d)\n", + min_x, max_x, min_y, max_y); +} + +// 计算坐标转换矩阵的函数(预留算法接口) +void calculate_ctm(ScreenInfo screen, int total_width, int total_height, float matrix[9]) { + float scale_x = (float)screen.width / total_width; + float scale_y = (float)screen.height / total_height; + float offset_x = (float)screen.x / total_width; + float offset_y = (float)screen.y / total_height; + + // 设置变换矩阵 + matrix[0] = scale_x; matrix[1] = 0.0f; matrix[2] = offset_x; + matrix[3] = 0.0f; matrix[4] = scale_y; matrix[5] = offset_y; + matrix[6] = 0.0f; matrix[7] = 0.0f; matrix[8] = 1.0f; +} + +// 设置设备的坐标转换矩阵 +int set_ctm(int device_id, float matrix[9]) { + Display *display = XOpenDisplay(NULL); + if (!display) { + fprintf(stderr, "无法打开X显示连接\n"); + return 0; + } + + // 创建ATOM属性 + Atom prop = XInternAtom(display, "Coordinate Transformation Matrix", False); + if (!prop) { + fprintf(stderr, "无法创建Coordinate Transformation Matrix属性\n"); + XCloseDisplay(display); + return 0; + } + + // 检查设备是否支持此属性 + int num_props = 0; + Atom *props = XIListProperties(display, device_id, &num_props); + int supports_matrix = 0; + + if (props) { + for (int i = 0; i < num_props; i++) { + char *prop_name = XGetAtomName(display, props[i]); + if (strcmp(prop_name, "Coordinate Transformation Matrix") == 0) { + supports_matrix = 1; + XFree(prop_name); + break; + } + XFree(prop_name); + } + XFree(props); + } + + if (!supports_matrix) { + fprintf(stderr, "设备 %d 不支持坐标转换矩阵属性\n", device_id); + XCloseDisplay(display); + return 0; + } + + // 创建FLOAT类型的原子 + Atom float_atom = XInternAtom(display, "FLOAT", False); + if (!float_atom) { + fprintf(stderr, "无法创建FLOAT类型原子\n"); + XCloseDisplay(display); + return 0; + } + + // 设置属性值 + XIChangeProperty(display, device_id, prop, float_atom, 32, PropModeReplace, + (unsigned char*)matrix, 9); + + XFlush(display); + XCloseDisplay(display); + return 1; +} + +// // 释放设备信息结构体内存 +// void free_input_devices(struct InputDeviceInfo *devices, int num_devices) { +// for (int i = 0; i < num_devices; i++) { +// free(devices[i].name); +// free(devices[i].device_node); +// free(devices[i].vid); +// free(devices[i].pid); +// free(devices[i].usb_path); +// } +// free(devices); +// } + +// 示例使用 +int main(int argc, char *argv[]) { + char *output_file_path = NULL; + + // 处理命令行参数 + if (argc > 1) { + output_file_path = argv[1]; + log_message("输出文件路径: %s\n", output_file_path); + } + + // 打开日志文件 + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("警告: 无法打开日志文件 /opt/ktouch/sub_modules.log,仅输出到控制台\n"); + } + + log_message("开始触摸屏配置...\n"); + + TouchConfig configs[MAX_DEVICES]; + ScreenInfo screens[MAX_DEVICES]; + InputDeviceInfo devices[MAX_DEVICES]; + + int config_count = read_config("/opt/ktouch/config", configs, MAX_DEVICES); + if (config_count < 0) { + log_message("读取配置文件失败\n"); + if (log_file) fclose(log_file); + return 1; + } + log_message("读取了 %d 个配置项\n", config_count); + + int screen_count = read_screen_info("/tmp/ktouch/screen.txt", screens, MAX_DEVICES); + if (screen_count < 0) { + log_message("读取屏幕信息文件失败\n"); + if (log_file) fclose(log_file); + return 1; + } + log_message("读取了 %d 个屏幕信息\n", screen_count); + + // 计算虚拟桌面大小 + int total_width, total_height; + calculate_virtual_desktop(screens, screen_count, &total_width, &total_height); + log_message("虚拟桌面大小: %dx%d\n", total_width, total_height); + + int device_count = 0; + device_count = get_input_devices(devices); + log_message("找到了 %d 个触摸设备\n", device_count); + + // 输出所有找到的设备信息 + for (int i = 0; i < device_count; i++) { + log_message("设备 %d: %s (VID:%s PID:%s 路径:%s XInput ID:%d)\n", + i, devices[i].name, devices[i].vid, devices[i].pid, + devices[i].usb_path, devices[i].device_id); + } + + // 对每个配置项查找匹配的设备 + for (int i = 0; i < config_count; i++) { + log_message("处理配置: %s|%s|%s|%s|%s\n", + configs[i].display_name, configs[i].touchscreen_name, + configs[i].vid, configs[i].pid, configs[i].usb_path); + + int device_index = find_matching_device(configs[i], devices, device_count); + if (device_index == -1) { + log_message("未找到匹配 %s 的设备\n", configs[i].touchscreen_name); + continue; + } + + log_message("匹配设备: %s (VID:%s PID:%s 路径:%s XInput ID:%d)\n", + devices[device_index].name, devices[device_index].vid, + devices[device_index].pid, devices[device_index].usb_path, + devices[device_index].device_id); + devices[device_index].match_success = 1; + + // 检查XInput设备ID是否有效 + if (devices[device_index].device_id == -1) { + log_message("错误: 设备 %s 没有有效的XInput ID\n", devices[device_index].name); + continue; + } + + // 查找对应的屏幕信息 + ScreenInfo* matched_screen = NULL; + for (int j = 0; j < screen_count; j++) { + if (strcmp(screens[j].name, configs[i].display_name) == 0) { + matched_screen = &screens[j]; + break; + } + } + + if (matched_screen == NULL) { + log_message("未找到 %s 对应的屏幕, 将使用第一屏\n", configs[i].display_name); + // 未找到屏幕,则返回第一个屏幕 + matched_screen = &screens[0]; + } + + log_message("匹配屏幕: %s %dx%d 位置(%d,%d)\n", + matched_screen->name, matched_screen->width, + matched_screen->height, matched_screen->x, matched_screen->y); + + // 计算并设置矩阵 + float matrix[9]; + calculate_ctm(*matched_screen, total_width, total_height, matrix); + + log_message("计算矩阵: [%f, %f, %f, %f, %f, %f, %f, %f, %f]\n", + matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5], + matrix[6], matrix[7], matrix[8]); + + set_ctm(devices[device_index].device_id, matrix); + memcpy(devices[device_index].matrix, matrix, sizeof(float) * 9); + + log_message("已配置 %s 为 %s 的触摸屏\n", + devices[device_index].name, configs[i].display_name); + } + + // 保存到文件 + if (output_file_path) { + int max_retries = 3; + int retry_delay = 1; // 秒 + FILE *output_file = NULL; + + for (int retry = 0; retry < max_retries; retry++) { + output_file = fopen(output_file_path, "w"); + if (output_file) { + break; + } + log_message("无法打开输出文件 %s (尝试 %d/%d),%d秒后重试...\n", + output_file_path, retry + 1, max_retries, retry_delay); + sleep(retry_delay); + } + + if (output_file) { + int wcount = 0; + for (int i = 0; i < device_count; i++) { + wcount++; + if(devices[i].match_success == 0){ + continue; + } + fprintf(output_file, "%d|%s|%s|%s:%s|%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f\n", + devices[i].device_id, + devices[i].name, + devices[i].device_node, + devices[i].vid, + devices[i].pid, + devices[i].matrix[0], devices[i].matrix[1], devices[i].matrix[2], + devices[i].matrix[3], devices[i].matrix[4], devices[i].matrix[5], + devices[i].matrix[6], devices[i].matrix[7], devices[i].matrix[8]); + } + fclose(output_file); + log_message("已成功写入 %d 条记录到文件 %s\n", wcount, output_file_path); + } else { + log_message("经过 %d 次尝试后仍无法打开输出文件 %s,放弃写入\n", + max_retries, output_file_path); + } + } + + log_message("触摸屏配置完成\n"); + + if (log_file) { + fclose(log_file); + } + + return 0; + +} \ No newline at end of file diff --git a/opt/ktouch/src/usb_ds.c b/opt/ktouch/src/usb_ds.c new file mode 100644 index 0000000..8c22e70 --- /dev/null +++ b/opt/ktouch/src/usb_ds.c @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_POLL_INTERVAL 5 // 默认轮询间隔为5秒 +#define MAX_RETRY_ATTEMPTS 3 // 最大重试次数 +#define RETRY_DELAY 1 // 重试延迟(秒) + +FILE *log_file = NULL; // 日志文件指针 +// 函数提前声明 +int write_file_content_with_retry(const char *file_path, int value); +void sigint_handler(int sig); +int is_touchscreen(struct udev_device *dev); +int read_file_content_with_retry(const char *file_path); +void monitor_usb_devices(const char *control_file_path, int poll_interval); +void print_usage(const char *program_name); + + + +volatile sig_atomic_t keep_running = 1; + +void sigint_handler(int sig) { + keep_running = 0; +} + +// 日志函数 +void log_message(const char *format, ...) { + va_list args; + va_start(args, format); + + // 获取当前时间 + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_str[20]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + // 输出到控制台 + printf("[%s] usb_ds \t", time_str); + vprintf(format, args); + + // 输出到文件 + if (log_file) { + fprintf(log_file, "[%s] usb_ds \t", time_str); + vfprintf(log_file, format, args); + fflush(log_file); // 确保立即写入文件 + } + + va_end(args); +} + + + +// 检查设备是否为触摸屏(简化版) +int is_touchscreen(struct udev_device *dev) { + const char *name = udev_device_get_property_value(dev, "NAME"); + const char *id_input_touchscreen = udev_device_get_property_value(dev, "ID_INPUT_TOUCHSCREEN"); + + // 检查ID_INPUT_TOUCHSCREEN属性 + if (id_input_touchscreen != NULL && strcmp(id_input_touchscreen, "1") == 0) { + return 1; + } + + // 检查设备名称中是否包含"touch"相关关键词(不区分大小写) + if (name != NULL) { + // 转换为小写以进行不区分大小写的比较 + char lower_name[256]; + strncpy(lower_name, name, sizeof(lower_name) - 1); + lower_name[sizeof(lower_name) - 1] = '\0'; + + for (int i = 0; lower_name[i]; i++) { + lower_name[i] = tolower(lower_name[i]); + } + + if (strstr(lower_name, "touchscreen") != NULL || + strstr(lower_name, "touch") != NULL || + strstr(lower_name, "tablet") != NULL) { + return 1; + } + } + + return 0; +} + +// 读取文件内容(带重试机制) +int read_file_content_with_retry(const char *file_path) { + int attempts = 0; + int content = -1; + + while (attempts < MAX_RETRY_ATTEMPTS && keep_running) { + FILE *file = fopen(file_path, "r"); + if (file == NULL) { + if (errno == ENOENT) { + // 文件不存在,创建并初始化为0 + printf("控制文件不存在,创建文件并初始化为0\n"); + if (write_file_content_with_retry(file_path, 0) == 0) { + content = 0; + break; + } + } else { + perror("打开文件失败"); + } + } else { + if (fscanf(file, "%d", &content) == 1) { + fclose(file); + break; + } else { + fclose(file); + fprintf(stderr, "读取文件内容失败\n"); + } + } + + attempts++; + if (attempts < MAX_RETRY_ATTEMPTS) { + printf("重试读取文件(%d/%d)...\n", attempts, MAX_RETRY_ATTEMPTS); + sleep(RETRY_DELAY); + } + } + + if (attempts >= MAX_RETRY_ATTEMPTS) { + fprintf(stderr, "达到最大重试次数,放弃读取文件\n"); + } + + return content; +} + +// 写入文件内容(带重试机制) +int write_file_content_with_retry(const char *file_path, int value) { + int attempts = 0; + int success = -1; + + while (attempts < MAX_RETRY_ATTEMPTS && keep_running) { + FILE *file = fopen(file_path, "w"); + if (file == NULL) { + perror("打开文件失败"); + } else { + if (fprintf(file, "%d", value) > 0) { + success = 0; + fclose(file); + break; + } else { + fclose(file); + fprintf(stderr, "写入文件内容失败\n"); + } + } + + attempts++; + if (attempts < MAX_RETRY_ATTEMPTS) { + printf("重试写入文件(%d/%d)...\n", attempts, MAX_RETRY_ATTEMPTS); + sleep(RETRY_DELAY); + } + } + + if (attempts >= MAX_RETRY_ATTEMPTS) { + fprintf(stderr, "达到最大重试次数,放弃写入文件\n"); + } + + return success; +} + +// 监控USB设备事件 +void monitor_usb_devices(const char *control_file_path, int poll_interval) { + struct udev *udev; + struct udev_monitor *mon; + struct udev_device *dev; + int fd; + int use_control_file = 0; + + // 检查是否使用控制文件 + if (control_file_path != NULL) { + use_control_file = 1; + log_message("使用控制文件: %s\n", control_file_path); + + // 初始化控制文件(如果不存在) + if (access(control_file_path, F_OK) != 0) { + printf("控制文件不存在,创建并初始化为0\n"); + if (write_file_content_with_retry(control_file_path, 0) != 0) { + fprintf(stderr, "无法创建控制文件,禁用控制文件功能\n"); + use_control_file = 0; + } + } + } + + // 创建udev对象 + udev = udev_new(); + if (!udev) { + // fprintf(stderr, "无法创建udev对象\n"); + exit(EXIT_FAILURE); + } + + // 创建监控对象,监控USB设备 + mon = udev_monitor_new_from_netlink(udev, "udev"); + udev_monitor_filter_add_match_subsystem_devtype(mon, "usb", NULL); + udev_monitor_filter_add_match_subsystem_devtype(mon, "input", NULL); + udev_monitor_enable_receiving(mon); + + // 获取监控文件描述符 + fd = udev_monitor_get_fd(mon); + + printf("开始监控USB设备...\n"); + printf("轮询间隔: %d秒\n", poll_interval); + printf("按Ctrl+C退出\n\n"); + + // 主循环,监控设备事件 + while (keep_running) { + fd_set fds; + struct timeval tv; + int ret; + int file_status = 0; + int touchscreen_detected = 0; + + // 如果使用控制文件,在每轮查询开始前检查文件状态 + if (use_control_file) { + file_status = read_file_content_with_retry(control_file_path); + if (file_status == -1) { + // 读取文件失败,继续监控但不处理事件 + log_message("读取控制文件失败,继续监控但不处理事件\n"); + sleep(poll_interval); + continue; + } + + if (file_status != 0) { + // 文件状态不为0,不处理事件 + printf("控制文件状态为%d,不处理事件\n", file_status); + sleep(poll_interval); + continue; + } + } + + FD_ZERO(&fds); + FD_SET(fd, &fds); + tv.tv_sec = poll_interval; + tv.tv_usec = 0; + + ret = select(fd+1, &fds, NULL, NULL, &tv); + if (ret > 0 && FD_ISSET(fd, &fds)) { + // 获取设备 + dev = udev_monitor_receive_device(mon); + if (dev) { + const char *action = udev_device_get_action(dev); + const char *devpath = udev_device_get_devpath(dev); + const char *product = udev_device_get_property_value(dev, "ID_MODEL"); + const char *vendor = udev_device_get_property_value(dev, "ID_VENDOR"); + + // 只处理添加设备的事件 + if (action && strcmp(action, "add") == 0) { + log_message("检测到新设备:\n"); + log_message(" 设备路径: %s, %s:%s\n", devpath, vendor ? vendor : "未知",product ? product : "未知"); + + // 检查是否为指针设备 + const char *id_input_mouse = udev_device_get_property_value(dev, "ID_INPUT_MOUSE"); + const char *id_input_touchpad = udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD"); + const char *id_input_joystick = udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK"); + const char *id_input_touchscreen = udev_device_get_property_value(dev, "ID_INPUT_TOUCHSCREEN"); + + if (id_input_mouse != NULL || id_input_touchpad != NULL || id_input_joystick != NULL || id_input_touchscreen != NULL) { + printf(" 类型: 指针设备\n"); + + // 检查是否为触摸屏 + if (is_touchscreen(dev)) { + printf(" 子类型: 触摸屏\n"); + touchscreen_detected = 1; + } + } + + // printf("\n"); + } + + udev_device_unref(dev); + } + } else if (ret == 0) { + // 超时,没有事件发生 + if (use_control_file) { + // 检查控制文件状态 + int current_status = read_file_content_with_retry(control_file_path); + if (current_status == -1) { + printf("读取控制文件失败\n"); + } else if (current_status != file_status) { + printf("控制文件状态已从%d变为%d\n", file_status, current_status); + } + } + } + + // 一轮检测完成后,如果需要更新控制文件状态 + if (use_control_file && touchscreen_detected) { + if (write_file_content_with_retry(control_file_path, 1) == 0) { + log_message("已更新控制文件状态为1\n"); + + // 等待文件状态恢复为0 + printf("等待控制文件状态恢复为0...\n"); + while (keep_running) { + int current_status = read_file_content_with_retry(control_file_path); + if (current_status == 0) { + printf("控制文件状态已恢复为0,继续监控\n"); + break; + } else if (current_status == -1) { + printf("读取控制文件失败,等待%d秒后重试\n", poll_interval); + sleep(poll_interval); + } else { + printf("当前控制文件状态: %d,等待%d秒后检查\n", current_status, poll_interval); + sleep(poll_interval); + } + } + } else { + log_message("更新控制文件状态失败\n"); + } + } + } + + // 清理资源 + udev_monitor_unref(mon); + udev_unref(udev); +} + +void print_usage(const char *program_name) { + printf("用法: %s [控制文件路径] [轮询间隔(秒)]\n", program_name); + printf("选项:\n"); + printf(" 控制文件路径: 用于控制监控的文件路径(如/tmp/usb_monitor.ctl)\n"); + printf(" 轮询间隔: 检查间隔时间(秒),默认%d秒\n", DEFAULT_POLL_INTERVAL); + printf("示例:\n"); + printf(" %s /tmp/usb_monitor.ctl 5\n", program_name); + printf(" %s\n", program_name); +} + +int main(int argc, char *argv[]) { + setlocale(LC_ALL, ""); + + const char *control_file_path = NULL; + int poll_interval = DEFAULT_POLL_INTERVAL; + + // 解析命令行参数 + if (argc > 1) { + if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { + print_usage(argv[0]); + return EXIT_SUCCESS; + } + + control_file_path = argv[1]; + } + + if (argc > 2) { + poll_interval = atoi(argv[2]); + if (poll_interval <= 0) { + fprintf(stderr, "轮询间隔必须大于0\n"); + return EXIT_FAILURE; + } + } + + log_file = fopen("/opt/ktouch/sub_modules.log", "a"); + if (!log_file) { + printf("无法打开日志文件,将只输出到控制台\n"); + } + + + log_message("USB触摸屏设备检测程序\n"); + printf("=====================\n"); + + // 设置信号处理 + signal(SIGINT, sigint_handler); + signal(SIGTERM, sigint_handler); + + // 启动监控 + monitor_usb_devices(control_file_path, poll_interval); + + log_message("程序已退出\n"); + return EXIT_SUCCESS; +} \ No newline at end of file