├── .gitignore ├── LICENSE ├── README.md ├── config.yaml ├── example └── getevent.py ├── image ├── color_exp_panel.png ├── color_pure_exp_panel.png ├── exp_monitor.jpg ├── exp_monitor_high.png ├── exp_panel.png ├── image_monitor.png └── image_monitor_high.png ├── install.bat ├── main.py ├── requirements.txt └── start.bat /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /__pycache__ 3 | /test/ 4 | /app/ 5 | /main_test.py 6 | /start_test.bat 7 | /start_wec.bat 8 | /config_wec.yaml 9 | /config_zgh.yaml 10 | /start_zgh.bat 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 illegal prompt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # maimai-android-touch-panel 2 | 3 | 使用 `adb shell getevent` 记录 Android 设备触屏事件并模拟 maimai 触摸屏幕的脚本. 4 | 5 | ## 提示 6 | 7 | 玩具项目, 仅在 Xiaomi Pad 5 Pro (Android 13) 上通过测试, 8 | 且仅适配了 Linux 多点触控协议类型 B . 9 | 10 | 目前已知的问题有: 11 | 12 | - 仅支持 Linux 多点触控协议类型 B 而不支持 A (#6), 这可能会导致较旧的设备不受支持, 13 | 两种类型不同之处详见[文档](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) 14 | - 输出 Touch Keys 但无按键按下(分辨率问题) 15 | - 游戏内按两下只识别一个 tap(脚本未进入运行模式) 16 | - 游戏内始终显示按下(未知原因) 17 | 18 | 本人暂无时间去修复存在的 Bug, 对于 open issue 和 B 站私信问题的很抱歉本人无法进行答复, 19 | 有能力的可以自行修复, 也欢迎提交 PR. 20 | 21 | 另外本项目使用了效率较为低下且抽象的方案(Python+读图+串流), 存在延迟等问题, 由于本身是娱乐项目故未做优化. 22 | 23 | 更加优秀的项目有: 24 | 25 | - [KanadeDX](https://github.com/KanadeDX/Public) (某八个按键程序在 Android/iOS 上的实现) 26 | - [AstroDX](https://github.com/2394425147/astrodx) (Android, Windows?) 27 | - [MajdataPlay](https://github.com/LingFeng-bbben/MajdataPlay) (Windows, Android?) 28 | 29 | 这些项目包含对 Mai2 Chart Player 的完整实现, 而不仅仅是一个触摸输入程序. 30 | 31 | ## 使用方法 32 | 33 | 1. 请先将游戏配置文件中 `DummyTouchPanel` 的值改为 `0` 34 | 2. 打开任意 P 图工具, 准备一个和设备屏幕大小相同的一张图片(例如:1600x2560), 将 `./image/color_exp_panel.png` 35 | 放置到该图片圆形触摸区域的位置, 编辑好的图片放到脚本 `image` 目录下取名 `image_monitor.png`. 36 | 3. 编辑 `config.yaml` 配置文件, 修改 `exp_image_dict` 配置, 将各区块对应的 RGB 通道颜色值改为刚 P 的图的对应区块颜色值( 37 | 一般不用改默认就行) 38 | 4. 电脑安装 ADB 调试工具, 安装路径添加到系统环境变量里面 39 | 5. 如果电脑上没有 Python 环境, 请先去 [官网](https://www.python.org/) 下载安装 40 | 6. 双击运行 `install.bat` 安装依赖 41 | 7. 先将实际屏幕大小填入脚本内 `ANDROID_ABS_MONITOR_SIZE` 配置, 打开终端, 运行 `adb shell getevent -l`, 点一下屏幕的最右下角的位置, 42 | 在终端获取该次点击得到的 `ABS_MT_POSITION_X` 和 `ABS_MT_POSITION_Y` 的数值, 把十六进制转换到十进制, 43 | 将得到的数据填入到 `ANDROID_ABS_INPUT_SIZE` 配置 44 | 8. Android 设备充电口朝下一般为屏幕的正向, 如需反向屏幕游玩可将配置 `ANDROID_REVERSE_MONITOR` 改为 true 45 | 9. 编辑 `config.yaml` 配置文件, 按文件内说明修改多个配置 46 | 10. 下载一个 `VSPD` 虚拟串口工具, 将 `COM3` 和 `COM33` 建立转发 47 | 11. 手机打开 USB 调试, 强烈建议同时使用 USB 网络共享连接电脑, 串流走 WLAN 可能不是很稳定 48 | 12. 电脑画面可使用 `IddSampleDriver`, `Sunshine` 和 `Moonlight` (提一嘴:想要竖屏串流必须使用支持竖屏的 Sunshine Nightly 49 | 版本, [Releases 地址](https://github.com/LizardByte/Sunshine/releases/nightly-dev)) 50 | 或者延迟较大但比较方便的 `spacedesk` 等软件串流到 Android 51 | 设备, 52 | 详细过程请自行寻找, 不在本篇讨论范围之内 53 | 13. 手机连接电脑, 先双击运行 `start.bat`, 再运行游戏, 脚本控制台输出 `已连接到游戏` 即可 54 | 14. 进游戏调整延迟, 一般判定 A/B 都要调才能正常用, 我这边是 `A:-1.0/B:+0.5` 到 `A:-2.0/B:+2.0` 55 | 15. 打一把看看蹭不蹭星星/触控是否灵敏, 根据体验修改 `AREA_SCOPE` 变量 c'c'x'c'c'z'z'z'z'd'd'd'd'c'x 56 | 16. 如果单点延迟低但滑动时延迟极大, 请将脚本中 `TOUCH_THREAD_SLEEP_MODE` 修改为 false, 57 | 或者可以调小 `TOUCH_THREAD_SLEEP_DELAY` 的值(如果还是卡请提 issue 反馈) 58 | 59 | 60 | ## 命令列表 61 | 62 | 游戏时如果不小心断开连接, 请在控制台输入 `start` 并回车来重新连接游戏 63 | 64 | 输入 `reverse` 可调整触控设备屏幕方向 65 | 66 | 输入 `restart` 可重新读取配置文件/重启脚本 67 | 68 | ## 部分问题 69 | 70 | 关于延迟/其他建议可参考 [#3](https://github.com/ERR0RPR0MPT/maimai-android-touch-panel/issues/3) 71 | 72 | Q: 在安卓高版本(13,14)上测试触摸区域完全对不上,只有点屏幕左上角有用,图片用的是平板实际分辨率,在一台安卓 10 设备测试是正常的 73 | 74 | A: 按步骤修改脚本内 `ANDROID_ABS_MONITOR_SIZE` 和 `ANDROID_ABS_INPUT_SIZE` 配置 75 | 76 | Q: 关闭再打开报错 77 | 78 | A: 如果直接关闭控制台窗口有可能导致后台进程残留,请使用任务管理器彻底关闭进程或者使用Ctrl+C终止程序 79 | 80 | ## 注意 81 | 82 | 想要加 2P 的重新复制一下脚本并添加串口 COM4 到 COM44 的转发,并且在配置文件“SPECIFIED_DEVICES”中指定使用“adb devices”获取到的设备序列号 83 | 84 | 该脚本仅用于测试, 目前来说打 12+及以下应该是问题不大, 12+以上水平不够没试过. 85 | 86 | ## 类似项目 87 | 88 | [maimai-windows-touch-panel](https://github.com/ERR0RPR0MPT/maimai-windows-touch-panel) 89 | 90 | ## 许可证 91 | 92 | [MIT License](https://github.com/ERR0RPR0MPT/maimai-android-touch-panel?tab=MIT-1-ov-file) 93 | 94 | ## 其他 95 | 96 | 编辑好的区块成品图类似这样: 97 | 98 | ![](https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/main/image/image_monitor.png) 99 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # 编辑好的图片路径 2 | IMAGE_PATH: "./image/image_monitor.png" 3 | # 串口号 4 | COM_PORT: "COM33" 5 | # 比特率 6 | COM_BAUDRATE: 9600 7 | # Android 多点触控数量 8 | MAX_SLOT: 12 9 | # 检测区域的像素值范围 10 | AREA_SCOPE: 50 11 | # 检测区域圆上点的数量 12 | AREA_POINT_NUM: 8 13 | # Android 设备实际屏幕大小 (单位:像素) 14 | ANDROID_ABS_MONITOR_SIZE: [ 1600, 2560 ] 15 | # Android 设备触控屏幕大小 (单位:像素) 16 | ANDROID_ABS_INPUT_SIZE: [ 1600, 2560 ] 17 | # 是否开启屏幕反转(充电口朝上时开启该配置) 18 | ANDROID_REVERSE_MONITOR: false 19 | # touch_thread 是否启用sleep, 默认关闭, 如果程序 CPU 占用较高则开启, 如果滑动时延迟极大请关闭 20 | TOUCH_THREAD_SLEEP_MODE: false 21 | # 每次 sleep 的延迟, 单位: 微秒, 默认 100 微秒 22 | TOUCH_THREAD_SLEEP_DELAY: 100 23 | #当需要指定触控设备时填上使用“adb devices”获取到的设备序列号,留空则只支持单设备连接 24 | SPECIFIED_DEVICES: "" 25 | 26 | # RGB 颜色值对应区块配置 27 | exp_image_dict: 28 | '41-65-93': A1 29 | '87-152-13': A2 30 | '213-109-81': A3 31 | '23-222-55': A4 32 | '69-203-71': A5 33 | '147-253-55': A6 34 | '77-19-35': A7 35 | '159-109-79': A8 36 | '87-217-111': B1 37 | '149-95-154': B2 38 | '97-233-9': B3 39 | '159-27-222': B4 40 | '152-173-186': B5 41 | '192-185-149': B6 42 | '158-45-23': B7 43 | '197-158-219': B8 44 | '127-144-79': C1 45 | '242-41-155': C2 46 | '69-67-213': D1 47 | '105-25-130': D2 48 | '17-39-170': D3 49 | '97-103-203': D4 50 | '113-25-77': D5 51 | '21-21-140': D6 52 | '155-179-166': D7 53 | '55-181-134': D8 54 | '61-33-27': E1 55 | '51-91-95': E2 56 | '143-227-63': E3 57 | '216-67-226': E4 58 | '202-181-245': E5 59 | '99-11-183': E6 60 | '75-119-224': E7 61 | '182-19-85': E8 62 | -------------------------------------------------------------------------------- /example/getevent.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import subprocess 3 | import copy 4 | import time 5 | 6 | max_slot = 12 7 | 8 | exp_image = Image.open("./image_monitor.png") 9 | exp_image_width, exp_image_height = exp_image.size 10 | 11 | exp_list = [ 12 | ["A1", "A2", "A3", "A4", "A5", ], 13 | ["A6", "A7", "A8", "B1", "B2", ], 14 | ["B3", "B4", "B5", "B6", "B7", ], 15 | ["B8", "C1", "C2", "D1", "D2", ], 16 | ["D3", "D4", "D5", "D6", "D7", ], 17 | ["D8", "E1", "E2", "E3", "E4", ], 18 | ["E5", "E6", "E7", "E8", ], 19 | ] 20 | exp_image_dict = { 21 | "61": "A1", "65": "A2", "71": "A3", "75": "A4", "81": "A5", "85": "A6", "91": "A7", "95": "A8", 22 | "101": "B1", "105": "B2", "111": "B3", "115": "B4", "121": "B5", "125": "B6", "130": "B7", "135": "B8", 23 | "140": "C1", "145": "C2", 24 | "150": "D1", "155": "D2", "160": "D3", "165": "D4", "170": "D5", "175": "D6", "180": "D7", "185": "D8", 25 | "190": "E1", "195": "E2", "200": "E3", "205": "E4", "210": "E5", "215": "E6", "220": "E7", "225": "E8", 26 | } 27 | 28 | 29 | def convert(touch_data): 30 | copy_exp_list = copy.deepcopy(exp_list) 31 | touch_keys = [] 32 | for i in touch_data: 33 | if not i["p"]: 34 | continue 35 | x = i["x"] 36 | y = i["y"] 37 | if 0 <= x < exp_image_width and 0 <= y < exp_image_height: 38 | rgb = exp_image.getpixel((x, y)) 39 | r_str = str(rgb[0]) 40 | if not r_str in exp_image_dict: 41 | continue 42 | touch_keys.append(exp_image_dict[r_str]) 43 | else: 44 | print("Coordinates ({}, {}) are out of image bounds.".format(x, y)) 45 | print("Touch Keys:", touch_keys) 46 | for i in range(len(copy_exp_list)): 47 | for j in range(len(copy_exp_list[i])): 48 | if copy_exp_list[i][j] in touch_keys: 49 | copy_exp_list[i][j] = 1 50 | else: 51 | copy_exp_list[i][j] = 0 52 | # print(copy_exp_list) 53 | 54 | 55 | 56 | def getevent(): 57 | # 存储多点触控数据的列表 58 | touch_data = [{"p": False, "x": 0, "y": 0} for _ in range(max_slot)] 59 | # 记录当前按下的触控点数目 60 | touch_sum = 0 61 | # 记录当前选择的 SLOT 作为索引 62 | touch_index = 0 63 | 64 | # 执行 adb shell getevent 命令并捕获输出 65 | process = subprocess.Popen(['adb', 'shell', 'getevent', '-l'], stdout=subprocess.PIPE) 66 | 67 | # 读取实时输出 68 | for line in iter(process.stdout.readline, b''): 69 | try: 70 | event = line.decode('utf-8').strip() 71 | _, _, event_type, event_value = event.split() 72 | if event_type == 'ABS_MT_POSITION_X': 73 | touch_data[touch_index]["x"] = int(event_value, 16) 74 | elif event_type == 'ABS_MT_POSITION_Y': 75 | touch_data[touch_index]["y"] = int(event_value, 16) 76 | elif event_type == 'SYN_REPORT': 77 | # print("Touch Data:", touch_data) 78 | # 向 convert 函数发送数据 79 | start_time = time.time() 80 | convert(touch_data) 81 | print(f"代码执行时间:{time.time() - start_time}秒") 82 | elif event_type == 'ABS_MT_SLOT': 83 | touch_index = int(event_value, 16) 84 | if touch_index >= touch_sum: 85 | touch_sum = touch_index + 1 86 | elif event_type == 'ABS_MT_TRACKING_ID': 87 | if event_value == "ffffffff": 88 | touch_data[touch_index]['p'] = False 89 | touch_sum -= 1 90 | if touch_sum < 0: 91 | touch_sum = 0 92 | else: 93 | touch_data[touch_index]['p'] = True 94 | touch_sum += 1 95 | else: 96 | continue 97 | except Exception: 98 | print(line.decode('utf-8')) 99 | pass 100 | 101 | 102 | if __name__ == "__main__": 103 | getevent() 104 | -------------------------------------------------------------------------------- /image/color_exp_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/color_exp_panel.png -------------------------------------------------------------------------------- /image/color_pure_exp_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/color_pure_exp_panel.png -------------------------------------------------------------------------------- /image/exp_monitor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/exp_monitor.jpg -------------------------------------------------------------------------------- /image/exp_monitor_high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/exp_monitor_high.png -------------------------------------------------------------------------------- /image/exp_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/exp_panel.png -------------------------------------------------------------------------------- /image/image_monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/image_monitor.png -------------------------------------------------------------------------------- /image/image_monitor_high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/b1b483a02380e5684c2cfea58ea5816bd31fc207/image/image_monitor_high.png -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pip install -r .\requirements.txt 3 | pause -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import subprocess 3 | import copy 4 | import time 5 | import threading 6 | import queue 7 | import serial 8 | import math 9 | import yaml 10 | import os 11 | import sys 12 | 13 | # 编辑好的图片路径 14 | IMAGE_PATH = "./image/image_monitor.png" 15 | # 串口号 16 | COM_PORT = "COM33" 17 | # 比特率 18 | COM_BAUDRATE = 9600 19 | # Android 多点触控数量 20 | MAX_SLOT = 12 21 | # 检测区域的像素值范围 22 | AREA_SCOPE = 50 23 | # 检测区域圆上点的数量 24 | AREA_POINT_NUM = 8 25 | # Android 设备实际屏幕大小 (单位:像素) 26 | ANDROID_ABS_MONITOR_SIZE = [1600, 2560] 27 | # Android 设备触控屏幕大小 (单位:像素) 28 | ANDROID_ABS_INPUT_SIZE = [1600, 2560] 29 | # 是否开启屏幕反转(充电口朝上时开启该配置) 30 | ANDROID_REVERSE_MONITOR = False 31 | # touch_thread 是否启用sleep, 默认关闭, 如果程序 CPU 占用较高则开启, 如果滑动时延迟极大请关闭 32 | TOUCH_THREAD_SLEEP_MODE = False 33 | # 每次 sleep 的延迟, 单位: 微秒, 默认 100 微秒 34 | TOUCH_THREAD_SLEEP_DELAY = 100 35 | #当需要指定触控设备时填上使用“adb devices”获取到的设备序列号,留空则只支持单设备连接 36 | SPECIFIED_DEVICES = "" 37 | 38 | exp_list = [ 39 | ["A1", "A2", "A3", "A4", "A5", ], 40 | ["A6", "A7", "A8", "B1", "B2", ], 41 | ["B3", "B4", "B5", "B6", "B7", ], 42 | ["B8", "C1", "C2", "D1", "D2", ], 43 | ["D3", "D4", "D5", "D6", "D7", ], 44 | ["D8", "E1", "E2", "E3", "E4", ], 45 | ["E5", "E6", "E7", "E8", ], 46 | ] 47 | exp_image_dict = {'41-65-93': 'A1', '87-152-13': 'A2', '213-109-81': 'A3', '23-222-55': 'A4', '69-203-71': 'A5', 48 | '147-253-55': 'A6', '77-19-35': 'A7', '159-109-79': 'A8', '87-217-111': 'B1', '149-95-154': 'B2', 49 | '97-233-9': 'B3', '159-27-222': 'B4', '152-173-186': 'B5', '192-185-149': 'B6', '158-45-23': 'B7', 50 | '197-158-219': 'B8', '127-144-79': 'C1', '242-41-155': 'C2', '69-67-213': 'D1', '105-25-130': 'D2', 51 | '17-39-170': 'D3', '97-103-203': 'D4', '113-25-77': 'D5', '21-21-140': 'D6', '155-179-166': 'D7', 52 | '55-181-134': 'D8', '61-33-27': 'E1', '51-91-95': 'E2', '143-227-63': 'E3', '216-67-226': 'E4', 53 | '202-181-245': 'E5', '99-11-183': 'E6', '75-119-224': 'E7', '182-19-85': 'E8'} 54 | 55 | 56 | class SerialManager: 57 | 58 | 59 | def __init__(self): 60 | self.p1Serial = serial.Serial(COM_PORT, COM_BAUDRATE) 61 | self.settingPacket = bytearray([40, 0, 0, 0, 0, 41]) 62 | self.startUp = False 63 | self.recvData = "" 64 | 65 | self.touchQueue = queue.Queue() 66 | self.data_lock = threading.Lock() 67 | self.touchThread = threading.Thread(target=self.touch_thread) 68 | self.writeThread = threading.Thread(target=self.write_thread) 69 | self.now_touch_data = b'' 70 | self.now_touch_keys = [] 71 | self.ping_touch_thread() 72 | 73 | def start(self): 74 | print(f"开始监听 {COM_PORT} 串口...") 75 | self.touchThread.start() 76 | self.writeThread.start() 77 | 78 | def ping_touch_thread(self): 79 | self.touchQueue.put([self.build_touch_package(exp_list), []]) 80 | 81 | def touch_thread(self): 82 | while True: 83 | # start_time = time.perf_counter() 84 | if self.p1Serial.is_open: 85 | self.read_data(self.p1Serial) 86 | if not self.touchQueue.empty(): 87 | # print("touchQueue 不为空,开始执行") 88 | s_temp = self.touchQueue.get() 89 | self.update_touch(s_temp) 90 | # 延迟防止消耗 CPU 时间过长 91 | if TOUCH_THREAD_SLEEP_MODE: 92 | microsecond_sleep(TOUCH_THREAD_SLEEP_DELAY) 93 | # print("单次执行时间:", (time.perf_counter() - start_time) * 1e3, "毫秒") 94 | 95 | def write_thread(self): 96 | while True: 97 | # # 延迟匹配波特率 98 | # time.sleep(0.0075) # 9600 99 | # # time.sleep(0.002) # 115200 100 | time.sleep(0.000001) # 避免延迟过大 101 | if not self.startUp: 102 | # print("当前没有启动") 103 | continue 104 | # print(self.now_touch_data) 105 | with self.data_lock: 106 | self.send_touch(self.p1Serial, self.now_touch_data) 107 | 108 | def destroy(self): 109 | self.touchThread.join() 110 | self.p1Serial.close() 111 | 112 | def read_data(self, ser): 113 | if ser.in_waiting == 6: 114 | self.recvData = ser.read(6).decode() 115 | # print(self.recvData) 116 | self.touch_setup(ser, self.recvData) 117 | 118 | def touch_setup(self, ser, data): 119 | byte_data = ord(data[3]) 120 | if byte_data in [76, 69]: 121 | self.startUp = False 122 | elif byte_data in [114, 107]: 123 | for i in range(1, 5): 124 | self.settingPacket[i] = ord(data[i]) 125 | ser.write(self.settingPacket) 126 | elif byte_data == 65: 127 | self.startUp = True 128 | print("已连接到游戏") 129 | 130 | def send_touch(self, ser, data): 131 | ser.write(data) 132 | 133 | # def build_touch_package(self, sl): 134 | # sum_list = [0, 0, 0, 0, 0, 0, 0] 135 | # for i in range(len(sl)): 136 | # for j in range(len(sl[i])): 137 | # if sl[i][j] == 1: 138 | # sum_list[i] += (2 ** j) 139 | # s = "28 " 140 | # for i in sum_list: 141 | # s += hex(i)[2:].zfill(2).upper() + " " 142 | # s += "29" 143 | # # print(s) 144 | # return bytes.fromhex(s) 145 | 146 | def build_touch_package(self, sl): 147 | sum_list = [sum(2 ** j for j, val in enumerate(row) if val == 1) for row in sl] 148 | hex_list = [hex(i)[2:].zfill(2).upper() for i in sum_list] 149 | s = "28 " + " ".join(hex_list) + " 29" 150 | # print(s) 151 | return bytes.fromhex(s) 152 | 153 | def update_touch(self, s_temp): 154 | # if not self.startUp: 155 | # print("当前没有启动") 156 | # return 157 | with self.data_lock: 158 | self.now_touch_data = s_temp[0] 159 | self.send_touch(self.p1Serial, s_temp[0]) 160 | self.now_touch_keys = s_temp[1] 161 | print("Touch Keys:", s_temp[1]) 162 | # else: 163 | # self.send_touch(self.p2Serial, s_temp[0]) 164 | 165 | def change_touch(self, sl, touch_keys): 166 | self.touchQueue.put([self.build_touch_package(sl), touch_keys]) 167 | 168 | 169 | def restart_script(): 170 | python = sys.executable 171 | script = os.path.abspath(sys.argv[0]) 172 | os.execv(python, [python, script]) 173 | 174 | 175 | def microsecond_sleep(sleep_time): 176 | end_time = time.perf_counter() + (sleep_time - 1.0) / 1e6 # 1.0是时间补偿,需要根据自己PC的性能去实测 177 | while time.perf_counter() < end_time: 178 | pass 179 | 180 | 181 | # 选择圆形区域的9个点作为判定 182 | def get_colors_in_area(x, y): 183 | colors = set() # 使用集合来存储颜色值,以避免重复 184 | num_points = AREA_POINT_NUM # 要获取的点的数量 185 | angle_increment = 360.0 / num_points # 角度增量 186 | cos_values = [math.cos(math.radians(i * angle_increment)) for i in range(num_points)] 187 | sin_values = [math.sin(math.radians(i * angle_increment)) for i in range(num_points)] 188 | # 处理中心点 189 | if 0 <= x < exp_image_width and 0 <= y < exp_image_height: 190 | colors.add(get_color_name(exp_image.getpixel((x, y)))) 191 | # 处理圆上的点 192 | for i in range(num_points): 193 | dx = int(AREA_SCOPE * cos_values[i]) 194 | dy = int(AREA_SCOPE * sin_values[i]) 195 | px = x + dx 196 | py = y + dy 197 | if 0 <= px < exp_image_width and 0 <= py < exp_image_height: 198 | colors.add(get_color_name(exp_image.getpixel((px, py)))) 199 | return list(colors) 200 | 201 | 202 | def get_color_name(pixel): 203 | return str(pixel[0]) + "-" + str(pixel[1]) + "-" + str(pixel[2]) 204 | 205 | 206 | def convert(touch_data): 207 | copy_exp_list = copy.deepcopy(exp_list) 208 | touch_keys = {exp_image_dict[rgb_str] for i in touch_data if i["p"] for rgb_str in 209 | get_colors_in_area(i["x"], i["y"]) if 210 | rgb_str in exp_image_dict} 211 | # print("Touch Keys:", touch_keys) 212 | # touched = sum(1 for i in touch_data if i["p"]) 213 | # print("Touched:", touched) 214 | touch_keys_list = list(touch_keys) 215 | copy_exp_list = [[1 if item in touch_keys_list else 0 for item in sublist] for sublist in copy_exp_list] 216 | # print(copy_exp_list) 217 | serial_manager.change_touch(copy_exp_list, touch_keys_list) 218 | 219 | 220 | # def convert(touch_data): 221 | # copy_exp_list = copy.deepcopy(exp_list) 222 | # touch_keys = set() 223 | # touched = 0 224 | # for i in touch_data: 225 | # if not i["p"]: 226 | # continue 227 | # touched += 1 228 | # x = i["x"] 229 | # y = i["y"] 230 | # for rgb_str in get_colors_in_area(x, y): 231 | # if not rgb_str in exp_image_dict: 232 | # continue 233 | # touch_keys.add(exp_image_dict[rgb_str]) 234 | # # print("Touched:", touched) 235 | # # print("Touch Keys:", touch_keys) 236 | # touch_keys_list = list(touch_keys) 237 | # for i in range(len(copy_exp_list)): 238 | # for j in range(len(copy_exp_list[i])): 239 | # if copy_exp_list[i][j] in touch_keys_list: 240 | # copy_exp_list[i][j] = 1 241 | # else: 242 | # copy_exp_list[i][j] = 0 243 | # # print(copy_exp_list) 244 | # serial_manager.change_touch(copy_exp_list, touch_keys_list) 245 | 246 | 247 | def getevent(): 248 | # 存储多点触控数据的列表 249 | touch_data = [{"p": False, "x": 0, "y": 0} for _ in range(MAX_SLOT)] 250 | # 记录当前按下的触控点数目 251 | touch_sum = 0 252 | # 记录当前选择的 SLOT 作为索引 253 | touch_index = 0 254 | 255 | # 执行 adb shell getevent 命令并捕获输出 256 | adb_cmd = 'adb shell getevent -l' 257 | if SPECIFIED_DEVICES: 258 | adb_cmd = 'adb -s ' + SPECIFIED_DEVICES + ' shell getevent -l' 259 | process = subprocess.Popen(adb_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 260 | key_is_changed = False 261 | 262 | # 读取实时输出 263 | for line in iter(process.stdout.readline, b''): 264 | try: 265 | 266 | event = line.decode('utf-8').strip() 267 | parts = event.split() 268 | 269 | # 屏蔽没用的东西 270 | if len(parts) < 4: 271 | continue 272 | 273 | event_type = parts[2] 274 | event_value_hex = parts[3] 275 | event_value = int(event_value_hex, 16) 276 | 277 | if event_type == 'ABS_MT_POSITION_X': 278 | key_is_changed = True 279 | touch_data[touch_index]["x"] = (ANDROID_ABS_MONITOR_SIZE[0] - event_value * abs_multi_x) if ANDROID_REVERSE_MONITOR else event_value * abs_multi_x 280 | 281 | elif event_type == 'ABS_MT_POSITION_Y': 282 | key_is_changed = True 283 | touch_data[touch_index]["y"] = (ANDROID_ABS_MONITOR_SIZE[1] - event_value * abs_multi_y) if ANDROID_REVERSE_MONITOR else event_value * abs_multi_y 284 | 285 | elif event_type == 'SYN_REPORT': 286 | if key_is_changed: 287 | convert(touch_data) 288 | key_is_changed = False 289 | 290 | elif event_type == 'ABS_MT_SLOT': 291 | key_is_changed = True 292 | touch_index = event_value 293 | if touch_index >= touch_sum: 294 | touch_sum = touch_index + 1 295 | 296 | elif event_type == 'ABS_MT_TRACKING_ID': 297 | key_is_changed = True 298 | if event_value_hex == "ffffffff": 299 | touch_data[touch_index]['p'] = False 300 | touch_sum = max(0, touch_sum - 1) 301 | else: 302 | touch_data[touch_index]['p'] = True 303 | touch_sum += 1 304 | 305 | except Exception as e: 306 | event_error_output = line.decode('utf-8') 307 | if "name" not in event_error_output: 308 | continue 309 | print(event_error_output) 310 | 311 | 312 | if __name__ == "__main__": 313 | yaml_file_path = 'config.yaml' 314 | if len(sys.argv) > 1: 315 | yaml_file_path = sys.argv[1] 316 | if os.path.isfile(yaml_file_path): 317 | print("使用配置文件:", yaml_file_path) 318 | with open(yaml_file_path, 'r', encoding='utf-8') as file: 319 | c = yaml.safe_load(file) 320 | IMAGE_PATH = c["IMAGE_PATH"] 321 | COM_PORT = c["COM_PORT"] 322 | COM_BAUDRATE = c["COM_BAUDRATE"] 323 | MAX_SLOT = c["MAX_SLOT"] 324 | AREA_SCOPE = c["AREA_SCOPE"] 325 | AREA_POINT_NUM = c["AREA_POINT_NUM"] 326 | ANDROID_ABS_MONITOR_SIZE = c["ANDROID_ABS_MONITOR_SIZE"] 327 | ANDROID_ABS_INPUT_SIZE = c["ANDROID_ABS_INPUT_SIZE"] 328 | ANDROID_REVERSE_MONITOR = c["ANDROID_REVERSE_MONITOR"] 329 | TOUCH_THREAD_SLEEP_MODE = c["TOUCH_THREAD_SLEEP_MODE"] 330 | TOUCH_THREAD_SLEEP_DELAY = c["TOUCH_THREAD_SLEEP_DELAY"] 331 | exp_image_dict = c["exp_image_dict"] 332 | SPECIFIED_DEVICES = c["SPECIFIED_DEVICES"] 333 | else: 334 | print("未找到配置文件, 使用默认配置") 335 | 336 | exp_image = Image.open(IMAGE_PATH) 337 | exp_image_width, exp_image_height = exp_image.size 338 | abs_multi_x = ANDROID_ABS_MONITOR_SIZE[0] / ANDROID_ABS_INPUT_SIZE[0] 339 | abs_multi_y = ANDROID_ABS_MONITOR_SIZE[1] / ANDROID_ABS_INPUT_SIZE[1] 340 | print("当前触控区域X轴放大倍数:", abs_multi_x) 341 | print("当前触控区域Y轴放大倍数:", abs_multi_y) 342 | print("当前链接到端口:", COM_PORT) 343 | print(('已' if ANDROID_REVERSE_MONITOR else '未') + "开启屏幕反转") 344 | serial_manager = SerialManager() 345 | serial_manager.start() 346 | threading.Thread(target=getevent).start() 347 | while True: 348 | input_str = input().strip() 349 | if len(input_str) == 0: 350 | continue 351 | if input_str == 'start': 352 | serial_manager.startUp = True 353 | print("已连接到游戏") 354 | elif input_str == 'reverse': 355 | ANDROID_REVERSE_MONITOR = not ANDROID_REVERSE_MONITOR 356 | print("已" + ('开启' if ANDROID_REVERSE_MONITOR else '关闭') + "屏幕反转") 357 | elif input_str == 'restart': 358 | restart_script() 359 | else: 360 | print("未知的输入") 361 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | pyserial 3 | pyyaml -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python .\main.py 3 | --------------------------------------------------------------------------------