commit 9524458c2a9ab3de7332719308b1b70034fbbc5b Author: ERR0RPR0MPT Date: Tue Apr 16 06:17:57 2024 +0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a099911 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea +/__pycache__ +/start_test.bat +/start_wec.bat +/config_wec.yaml +/config_zgh.yaml +/start_zgh.bat diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4aa98b6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 illegal prompt + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5d1780 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# maimai-windows-touch-panel + +记录 Windows 设备的触屏事件并模拟 maimai 分区触摸屏幕的脚本. + +软件模拟分区触摸, 可用于 maimai 自制手台上. + +## 使用方法 + +1. Windows 设置 - 蓝牙和其他设备 - 触控 - 三指和四指的触摸手势 - 关闭 +2. 辅助功能 - 鼠标指针与触控 - 触控指示器 - 关闭 +3. 控制面板 - 硬件和声音 - 笔和触控 - 关闭"触摸回应"中的"触摸屏幕时显示直观回应" +4. 打开任意P图工具, 准备一个和显示屏幕大小相同的一张图片(例如:2160x3840), 将 `./image/color_exp_panel.png` + 放置到该图片圆形触摸区域的位置, 编辑好的图片放到脚本 `image` 目录下取名 `image_monitor.png`. +5. 编辑 `config.yaml` 配置文件, 修改 `exp_image_dict` 配置, 将各区块对应的 RGB 通道颜色值改为刚P的图的对应区块颜色值( + 一般不用改默认就行) +6. 执行 `pip install -r ./requirements.txt` 安装依赖 +7. 编辑 `config.yaml` 配置文件 +8. 下载一个 `VSPD` 虚拟串口工具, 将 `COM3` 和 `COM33` 建立转发 +9. 触摸屏连接到电脑, 先双击运行 `start.bat`, 再运行游戏, 脚本控制台输出 `已连接到游戏` 即可 +10. 进游戏调整判定A/B的延迟直到可用 +11. 打一把看看蹭不蹭星星/触控是否灵敏, 根据体验修改 `AREA_SCOPE` 变量 +12. 如果单点延迟低但滑动时延迟变高, 请将脚本中 `TOUCH_THREAD_SLEEP_MODE` 修改为 false, + 或者可以调小 `TOUCH_THREAD_SLEEP_DELAY` 的值(如果还是卡请提 issue 反馈) + +## 命令列表 + +游戏时如果不小心断开连接, 请在控制台输入 `start` 并回车来重新连接游戏 + +输入 `reverse` 可调整触控设备屏幕方向 + +输入 `restart` 可重新读取配置文件/重启脚本 + +## 注意 + +想要加 2P 的重新复制一下脚本并添加串口 COM4 到 COM44 的转发就好 + +该脚本仅用于测试. + +## 类似项目 + +[maimai-android-touch-panel](https://github.com/ERR0RPR0MPT/maimai-android-touch-panel) + +## 许可证 + +[MIT License](https://github.com/ERR0RPR0MPT/maimai-windows-touch-panel?tab=MIT-1-ov-file) + +## 其他 + +编辑好的区块成品图类似这样: + +![](https://raw.githubusercontent.com/ERR0RPR0MPT/maimai-android-touch-panel/main/image/image_monitor.png) diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..3376104 --- /dev/null +++ b/config.yaml @@ -0,0 +1,55 @@ +# 编辑好的图片路径 +IMAGE_PATH: "./image/image_monitor_high.png" +# 串口号 +COM_PORT: "COM33" +# 比特率 +COM_BAUDRATE: 9600 +# 检测区域的像素值范围 +AREA_SCOPE: 50 +# 检测区域圆上点的数量 +AREA_POINT_NUM: 8 +# 触摸屏幕大小 (单位:像素) +MONITOR_SIZE: [ 1600, 2560 ] +# 是否开启屏幕反转 +REVERSE_MONITOR: false +# touch_thread 是否启用sleep, 默认开启, 如果程序 CPU 占用较高则开启, 如果滑动时延迟极大请关闭 +TOUCH_THREAD_SLEEP_MODE: true +# 每次 sleep 的延迟, 单位: 微秒, 默认 10 微秒 +TOUCH_THREAD_SLEEP_DELAY: 10 + +# RGB 颜色值对应区块配置 +exp_image_dict: + '41-65-93': A1 + '87-152-13': A2 + '213-109-81': A3 + '23-222-55': A4 + '69-203-71': A5 + '147-253-55': A6 + '77-19-35': A7 + '159-109-79': A8 + '87-217-111': B1 + '149-95-154': B2 + '97-233-9': B3 + '159-27-222': B4 + '152-173-186': B5 + '192-185-149': B6 + '158-45-23': B7 + '197-158-219': B8 + '127-144-79': C1 + '242-41-155': C2 + '69-67-213': D1 + '105-25-130': D2 + '17-39-170': D3 + '97-103-203': D4 + '113-25-77': D5 + '21-21-140': D6 + '155-179-166': D7 + '55-181-134': D8 + '61-33-27': E1 + '51-91-95': E2 + '143-227-63': E3 + '216-67-226': E4 + '202-181-245': E5 + '99-11-183': E6 + '75-119-224': E7 + '182-19-85': E8 diff --git a/image/color_exp_panel.png b/image/color_exp_panel.png new file mode 100644 index 0000000..c395d19 Binary files /dev/null and b/image/color_exp_panel.png differ diff --git a/image/color_pure_exp_panel.png b/image/color_pure_exp_panel.png new file mode 100644 index 0000000..40d3965 Binary files /dev/null and b/image/color_pure_exp_panel.png differ diff --git a/image/exp_monitor.jpg b/image/exp_monitor.jpg new file mode 100644 index 0000000..afdc045 Binary files /dev/null and b/image/exp_monitor.jpg differ diff --git a/image/exp_monitor_high.png b/image/exp_monitor_high.png new file mode 100644 index 0000000..60ba093 Binary files /dev/null and b/image/exp_monitor_high.png differ diff --git a/image/exp_panel.png b/image/exp_panel.png new file mode 100644 index 0000000..29a0830 Binary files /dev/null and b/image/exp_panel.png differ diff --git a/image/favicon.ico b/image/favicon.ico new file mode 100644 index 0000000..770feed Binary files /dev/null and b/image/favicon.ico differ diff --git a/image/image_monitor.png b/image/image_monitor.png new file mode 100644 index 0000000..968da55 Binary files /dev/null and b/image/image_monitor.png differ diff --git a/image/image_monitor_high.png b/image/image_monitor_high.png new file mode 100644 index 0000000..c758bce Binary files /dev/null and b/image/image_monitor_high.png differ diff --git a/image/main.py b/image/main.py new file mode 100644 index 0000000..9f7122d --- /dev/null +++ b/image/main.py @@ -0,0 +1,351 @@ +from PIL import Image +import subprocess +import copy +import time +import threading +import queue +import serial +import math +import yaml +import os +import sys + +# 编辑好的图片路径 +IMAGE_PATH = "./image/image_monitor.png" +# 串口号 +COM_PORT = "COM33" +# 比特率 +COM_BAUDRATE = 9600 +# Android 多点触控数量 +MAX_SLOT = 12 +# 检测区域的像素值范围 +AREA_SCOPE = 50 +# 检测区域圆上点的数量 +AREA_POINT_NUM = 8 +# Android 设备实际屏幕大小 (单位:像素) +ANDROID_ABS_MONITOR_SIZE = [1600, 2560] +# Android 设备触控屏幕大小 (单位:像素) +ANDROID_ABS_INPUT_SIZE = [1600, 2560] +# 是否开启屏幕反转(充电口朝上时开启该配置) +ANDROID_REVERSE_MONITOR = False +# touch_thread 是否启用sleep, 默认开启, 如果程序 CPU 占用较高则开启, 如果滑动时延迟极大请关闭 +TOUCH_THREAD_SLEEP_MODE = True +# 每次 sleep 的延迟, 单位: 微秒, 默认 100 微秒 +TOUCH_THREAD_SLEEP_DELAY = 100 + +exp_list = [ + ["A1", "A2", "A3", "A4", "A5", ], + ["A6", "A7", "A8", "B1", "B2", ], + ["B3", "B4", "B5", "B6", "B7", ], + ["B8", "C1", "C2", "D1", "D2", ], + ["D3", "D4", "D5", "D6", "D7", ], + ["D8", "E1", "E2", "E3", "E4", ], + ["E5", "E6", "E7", "E8", ], +] +exp_image_dict = {'41-65-93': 'A1', '87-152-13': 'A2', '213-109-81': 'A3', '23-222-55': 'A4', '69-203-71': 'A5', + '147-253-55': 'A6', '77-19-35': 'A7', '159-109-79': 'A8', '87-217-111': 'B1', '149-95-154': 'B2', + '97-233-9': 'B3', '159-27-222': 'B4', '152-173-186': 'B5', '192-185-149': 'B6', '158-45-23': 'B7', + '197-158-219': 'B8', '127-144-79': 'C1', '242-41-155': 'C2', '69-67-213': 'D1', '105-25-130': 'D2', + '17-39-170': 'D3', '97-103-203': 'D4', '113-25-77': 'D5', '21-21-140': 'D6', '155-179-166': 'D7', + '55-181-134': 'D8', '61-33-27': 'E1', '51-91-95': 'E2', '143-227-63': 'E3', '216-67-226': 'E4', + '202-181-245': 'E5', '99-11-183': 'E6', '75-119-224': 'E7', '182-19-85': 'E8'} + + +class SerialManager: + p1Serial = serial.Serial(COM_PORT, COM_BAUDRATE) + settingPacket = bytearray([40, 0, 0, 0, 0, 41]) + startUp = False + recvData = "" + + def __init__(self): + self.touchQueue = queue.Queue() + self.data_lock = threading.Lock() + self.touchThread = threading.Thread(target=self.touch_thread) + self.writeThread = threading.Thread(target=self.write_thread) + self.now_touch_data = b'' + self.now_touch_keys = [] + self.ping_touch_thread() + + def start(self): + print(f"开始监听 {COM_PORT} 串口...") + self.touchThread.start() + self.writeThread.start() + + def ping_touch_thread(self): + self.touchQueue.put([self.build_touch_package(exp_list), []]) + + def touch_thread(self): + while True: + start_time = time.perf_counter() + if self.p1Serial.is_open: + self.read_data(self.p1Serial) + if not self.touchQueue.empty(): + # print("touchQueue 不为空,开始执行") + s_temp = self.touchQueue.get() + self.update_touch(s_temp) + # 延迟防止消耗 CPU 时间过长 + if TOUCH_THREAD_SLEEP_MODE: + microsecond_sleep(TOUCH_THREAD_SLEEP_DELAY) + print("单次执行时间:", (time.perf_counter() - start_time) * 1e3, "毫秒") + + def write_thread(self): + while True: + # 延迟匹配波特率 + time.sleep(0.0075) # 9600 + # time.sleep(0.002) # 115200 + if not self.startUp: + # print("当前没有启动") + continue + # print(self.now_touch_data) + with self.data_lock: + self.send_touch(self.p1Serial, self.now_touch_data) + + def destroy(self): + self.touchThread.join() + self.p1Serial.close() + + def read_data(self, ser): + if ser.in_waiting == 6: + self.recvData = ser.read(6).decode() + # print(self.recvData) + self.touch_setup(ser, self.recvData) + + def touch_setup(self, ser, data): + byte_data = ord(data[3]) + if byte_data in [76, 69]: + self.startUp = False + elif byte_data in [114, 107]: + for i in range(1, 5): + self.settingPacket[i] = ord(data[i]) + ser.write(self.settingPacket) + elif byte_data == 65: + self.startUp = True + print("已连接到游戏") + + def send_touch(self, ser, data): + ser.write(data) + + # def build_touch_package(self, sl): + # sum_list = [0, 0, 0, 0, 0, 0, 0] + # for i in range(len(sl)): + # for j in range(len(sl[i])): + # if sl[i][j] == 1: + # sum_list[i] += (2 ** j) + # s = "28 " + # for i in sum_list: + # s += hex(i)[2:].zfill(2).upper() + " " + # s += "29" + # # print(s) + # return bytes.fromhex(s) + + def build_touch_package(self, sl): + sum_list = [sum(2 ** j for j, val in enumerate(row) if val == 1) for row in sl] + hex_list = [hex(i)[2:].zfill(2).upper() for i in sum_list] + s = "28 " + " ".join(hex_list) + " 29" + # print(s) + return bytes.fromhex(s) + + def update_touch(self, s_temp): + # if not self.startUp: + # print("当前没有启动") + # return + with self.data_lock: + self.now_touch_data = s_temp[0] + self.send_touch(self.p1Serial, s_temp[0]) + self.now_touch_keys = s_temp[1] + print("Touch Keys:", s_temp[1]) + # else: + # self.send_touch(self.p2Serial, s_temp[0]) + + def change_touch(self, sl, touch_keys): + self.touchQueue.put([self.build_touch_package(sl), touch_keys]) + + +def restart_script(): + python = sys.executable + script = os.path.abspath(sys.argv[0]) + os.execv(python, [python, script]) + + +def microsecond_sleep(sleep_time): + end_time = time.perf_counter() + (sleep_time - 1.0) / 1e6 # 1.0是时间补偿,需要根据自己PC的性能去实测 + while time.perf_counter() < end_time: + pass + + +# 选择圆形区域的9个点作为判定 +def get_colors_in_area(x, y): + colors = set() # 使用集合来存储颜色值,以避免重复 + num_points = AREA_POINT_NUM # 要获取的点的数量 + angle_increment = 360.0 / num_points # 角度增量 + cos_values = [math.cos(math.radians(i * angle_increment)) for i in range(num_points)] + sin_values = [math.sin(math.radians(i * angle_increment)) for i in range(num_points)] + # 处理中心点 + if 0 <= x < exp_image_width and 0 <= y < exp_image_height: + colors.add(get_color_name(exp_image.getpixel((x, y)))) + # 处理圆上的点 + for i in range(num_points): + dx = int(AREA_SCOPE * cos_values[i]) + dy = int(AREA_SCOPE * sin_values[i]) + px = x + dx + py = y + dy + if 0 <= px < exp_image_width and 0 <= py < exp_image_height: + colors.add(get_color_name(exp_image.getpixel((px, py)))) + return list(colors) + + +def get_color_name(pixel): + return str(pixel[0]) + "-" + str(pixel[1]) + "-" + str(pixel[2]) + + +def convert(touch_data): + copy_exp_list = copy.deepcopy(exp_list) + touch_keys = {exp_image_dict[rgb_str] for i in touch_data if i["p"] for rgb_str in get_colors_in_area(i["x"], i["y"]) if + rgb_str in exp_image_dict} + # print("Touch Keys:", touch_keys) + # touched = sum(1 for i in touch_data if i["p"]) + # print("Touched:", touched) + touch_keys_list = list(touch_keys) + copy_exp_list = [[1 if item in touch_keys_list else 0 for item in sublist] for sublist in copy_exp_list] + # print(copy_exp_list) + serial_manager.change_touch(copy_exp_list, touch_keys_list) + + +# def convert(touch_data): +# copy_exp_list = copy.deepcopy(exp_list) +# touch_keys = set() +# touched = 0 +# for i in touch_data: +# if not i["p"]: +# continue +# touched += 1 +# x = i["x"] +# y = i["y"] +# for rgb_str in get_colors_in_area(x, y): +# if not rgb_str in exp_image_dict: +# continue +# touch_keys.add(exp_image_dict[rgb_str]) +# # print("Touched:", touched) +# # print("Touch Keys:", touch_keys) +# touch_keys_list = list(touch_keys) +# for i in range(len(copy_exp_list)): +# for j in range(len(copy_exp_list[i])): +# if copy_exp_list[i][j] in touch_keys_list: +# copy_exp_list[i][j] = 1 +# else: +# copy_exp_list[i][j] = 0 +# # print(copy_exp_list) +# serial_manager.change_touch(copy_exp_list, touch_keys_list) + + +def getevent(): + # 存储多点触控数据的列表 + touch_data = [{"p": False, "x": 0, "y": 0} for _ in range(MAX_SLOT)] + # 记录当前按下的触控点数目 + touch_sum = 0 + # 记录当前选择的 SLOT 作为索引 + touch_index = 0 + + # 执行 adb shell getevent 命令并捕获输出 + process = subprocess.Popen(['adb', 'shell', 'getevent', '-l'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + key_is_changed = False + + # 读取实时输出 + for line in iter(process.stdout.readline, b''): + try: + event = line.decode('utf-8').strip() + _, _, event_type, event_value = event.split() + # print(event_type, int(event_value, 16)) + if event_type == 'ABS_MT_POSITION_X': + key_is_changed = True + if not ANDROID_REVERSE_MONITOR: + touch_data[touch_index]["x"] = int(int(event_value, 16) * abs_multi_x) + else: + touch_data[touch_index]["x"] = ANDROID_ABS_MONITOR_SIZE[0] - int(int(event_value, 16) * abs_multi_x) + elif event_type == 'ABS_MT_POSITION_Y': + key_is_changed = True + if not ANDROID_REVERSE_MONITOR: + touch_data[touch_index]["y"] = int(int(event_value, 16) * abs_multi_y) + else: + touch_data[touch_index]["y"] = ANDROID_ABS_MONITOR_SIZE[1] - int(int(event_value, 16) * abs_multi_y) + elif event_type == 'SYN_REPORT': + if not key_is_changed: + continue + # print("Touch Data:", touch_data) + # 向 convert 函数发送数据 + key_is_changed = False + # start_time = time.perf_counter() + convert(touch_data) + # print("单次执行时间:", (time.perf_counter() - start_time) * 1e3, "毫秒") + elif event_type == 'ABS_MT_SLOT': + key_is_changed = True + touch_index = int(event_value, 16) + if touch_index >= touch_sum: + touch_sum = touch_index + 1 + elif event_type == 'ABS_MT_TRACKING_ID': + key_is_changed = True + if event_value == "ffffffff": + touch_data[touch_index]['p'] = False + touch_sum = max(0, touch_sum - 1) + else: + touch_data[touch_index]['p'] = True + touch_sum += 1 + else: + continue + except Exception: + event_error_output = line.decode('utf-8') + if "name" in event_error_output: + continue + print(event_error_output) + + +exp_image = Image.open(IMAGE_PATH) +exp_image_width, exp_image_height = exp_image.size +abs_multi_x = 1 +abs_multi_y = 1 + +if __name__ == "__main__": + yaml_file_path = 'config.yaml' + if len(sys.argv) > 1: + yaml_file_path = sys.argv[1] + if os.path.isfile(yaml_file_path): + print("使用配置文件:", yaml_file_path) + with open(yaml_file_path, 'r', encoding='utf-8') as file: + c = yaml.safe_load(file) + IMAGE_PATH = c["IMAGE_PATH"] + COM_PORT = c["COM_PORT"] + COM_BAUDRATE = c["COM_BAUDRATE"] + MAX_SLOT = c["MAX_SLOT"] + AREA_SCOPE = c["AREA_SCOPE"] + AREA_POINT_NUM = c["AREA_POINT_NUM"] + ANDROID_ABS_MONITOR_SIZE = c["ANDROID_ABS_MONITOR_SIZE"] + ANDROID_ABS_INPUT_SIZE = c["ANDROID_ABS_INPUT_SIZE"] + ANDROID_REVERSE_MONITOR = c["ANDROID_REVERSE_MONITOR"] + TOUCH_THREAD_SLEEP_MODE = c["TOUCH_THREAD_SLEEP_MODE"] + TOUCH_THREAD_SLEEP_DELAY = c["TOUCH_THREAD_SLEEP_DELAY"] + exp_image_dict = c["exp_image_dict"] + else: + print("未找到配置文件, 使用默认配置") + + abs_multi_x = ANDROID_ABS_MONITOR_SIZE[0] / ANDROID_ABS_INPUT_SIZE[0] + abs_multi_y = ANDROID_ABS_MONITOR_SIZE[1] / ANDROID_ABS_INPUT_SIZE[1] + print("当前触控区域X轴放大倍数:", abs_multi_x) + print("当前触控区域Y轴放大倍数:", abs_multi_y) + print(('已' if ANDROID_REVERSE_MONITOR else '未') + "开启屏幕反转") + serial_manager = SerialManager() + serial_manager.start() + threading.Thread(target=getevent).start() + while True: + input_str = input().strip() + if len(input_str) == 0: + continue + if input_str == 'start': + serial_manager.startUp = True + print("已连接到游戏") + elif input_str == 'reverse': + ANDROID_REVERSE_MONITOR = not ANDROID_REVERSE_MONITOR + print("已" + ('开启' if ANDROID_REVERSE_MONITOR else '关闭') + "屏幕反转") + elif input_str == 'restart': + restart_script() + else: + print("未知的输入") diff --git a/main.py b/main.py new file mode 100644 index 0000000..c7bf225 --- /dev/null +++ b/main.py @@ -0,0 +1,324 @@ +from PIL import Image +import math +import os +import queue +import sys +import time +import yaml +import serial +import win32api +import win32con +import win32gui +import threading +import copy + +# 编辑好的图片路径 +IMAGE_PATH = "./image/image_monitor.png" +# 串口号 +COM_PORT = "COM33" +# 比特率 +COM_BAUDRATE = 9600 +# 检测区域的像素值范围 +AREA_SCOPE = 50 +# 检测区域圆上点的数量 +AREA_POINT_NUM = 8 +# 触摸屏幕大小 (单位:像素) +MONITOR_SIZE = [1600, 2560] +# 是否开启屏幕反转 +REVERSE_MONITOR = False +# touch_thread 是否启用sleep, 默认开启, 如果程序 CPU 占用较高则开启, 如果滑动时延迟极大请关闭 +TOUCH_THREAD_SLEEP_MODE = False +# 每次 sleep 的延迟, 单位: 微秒, 默认 10 微秒 +TOUCH_THREAD_SLEEP_DELAY = 10 +# 窗口图标路径 +icon_path = './image/favicon.ico' + +exp_list = [ + ["A1", "A2", "A3", "A4", "A5", ], + ["A6", "A7", "A8", "B1", "B2", ], + ["B3", "B4", "B5", "B6", "B7", ], + ["B8", "C1", "C2", "D1", "D2", ], + ["D3", "D4", "D5", "D6", "D7", ], + ["D8", "E1", "E2", "E3", "E4", ], + ["E5", "E6", "E7", "E8", ], +] +exp_image_dict = {'41-65-93': 'A1', '87-152-13': 'A2', '213-109-81': 'A3', '23-222-55': 'A4', '69-203-71': 'A5', + '147-253-55': 'A6', '77-19-35': 'A7', '159-109-79': 'A8', '87-217-111': 'B1', '149-95-154': 'B2', + '97-233-9': 'B3', '159-27-222': 'B4', '152-173-186': 'B5', '192-185-149': 'B6', '158-45-23': 'B7', + '197-158-219': 'B8', '127-144-79': 'C1', '242-41-155': 'C2', '69-67-213': 'D1', '105-25-130': 'D2', + '17-39-170': 'D3', '97-103-203': 'D4', '113-25-77': 'D5', '21-21-140': 'D6', '155-179-166': 'D7', + '55-181-134': 'D8', '61-33-27': 'E1', '51-91-95': 'E2', '143-227-63': 'E3', '216-67-226': 'E4', + '202-181-245': 'E5', '99-11-183': 'E6', '75-119-224': 'E7', '182-19-85': 'E8'} + + +class SerialManager: + p1Serial = serial.Serial(COM_PORT, COM_BAUDRATE) + settingPacket = bytearray([40, 0, 0, 0, 0, 41]) + startUp = False + recvData = "" + + def __init__(self): + self.touchQueue = queue.Queue() + self.data_lock = threading.Lock() + self.touchThread = threading.Thread(target=self.touch_thread) + self.writeThread = threading.Thread(target=self.write_thread) + self.now_touch_data = b'' + self.now_touch_keys = [] + self.ping_touch_thread() + + def start(self): + print(f"开始监听 {COM_PORT} 串口...") + self.touchThread.start() + self.writeThread.start() + + def ping_touch_thread(self): + self.touchQueue.put([self.build_touch_package(exp_list), []]) + + def touch_thread(self): + while True: + start_time = time.perf_counter() + if self.p1Serial.is_open: + self.read_data(self.p1Serial) + if not self.touchQueue.empty(): + # print("touchQueue 不为空,开始执行") + s_temp = self.touchQueue.get() + self.update_touch(s_temp) + # 延迟防止消耗 CPU 时间过长 + if TOUCH_THREAD_SLEEP_MODE: + microsecond_sleep(TOUCH_THREAD_SLEEP_DELAY) + # print("单次执行时间:", (time.perf_counter() - start_time) * 1e3, "毫秒") + + def write_thread(self): + while True: + # 延迟匹配波特率 + time.sleep(0.0075) # 9600 + # time.sleep(0.002) # 115200 + if not self.startUp: + # print("当前没有启动") + continue + # print(self.now_touch_data) + with self.data_lock: + self.send_touch(self.p1Serial, self.now_touch_data) + + def destroy(self): + self.touchThread.join() + self.p1Serial.close() + + def read_data(self, ser): + if ser.in_waiting == 6: + self.recvData = ser.read(6).decode() + # print(self.recvData) + self.touch_setup(ser, self.recvData) + + def touch_setup(self, ser, data): + byte_data = ord(data[3]) + if byte_data in [76, 69]: + self.startUp = False + elif byte_data in [114, 107]: + for i in range(1, 5): + self.settingPacket[i] = ord(data[i]) + ser.write(self.settingPacket) + elif byte_data == 65: + self.startUp = True + print("已连接到游戏") + + def send_touch(self, ser, data): + ser.write(data) + + # def build_touch_package(self, sl): + # sum_list = [0, 0, 0, 0, 0, 0, 0] + # for i in range(len(sl)): + # for j in range(len(sl[i])): + # if sl[i][j] == 1: + # sum_list[i] += (2 ** j) + # s = "28 " + # for i in sum_list: + # s += hex(i)[2:].zfill(2).upper() + " " + # s += "29" + # # print(s) + # return bytes.fromhex(s) + + def build_touch_package(self, sl): + sum_list = [sum(2 ** j for j, val in enumerate(row) if val == 1) for row in sl] + hex_list = [hex(i)[2:].zfill(2).upper() for i in sum_list] + s = "28 " + " ".join(hex_list) + " 29" + # print(s) + return bytes.fromhex(s) + + def update_touch(self, s_temp): + # if not self.startUp: + # print("当前没有启动") + # return + with self.data_lock: + self.now_touch_data = s_temp[0] + self.send_touch(self.p1Serial, s_temp[0]) + self.now_touch_keys = s_temp[1] + print("Touch Keys:", s_temp[1]) + # else: + # self.send_touch(self.p2Serial, s_temp[0]) + + def change_touch(self, sl, touch_keys): + self.touchQueue.put([self.build_touch_package(sl), touch_keys]) + + +def restart_script(): + python = sys.executable + script = os.path.abspath(sys.argv[0]) + os.execv(python, [python, script]) + + +def microsecond_sleep(sleep_time): + # time.sleep(sleep_time / 1000000) + end_time = time.perf_counter() + (sleep_time - 1.0) / 1e6 # 1.0是时间补偿,需要根据自己PC的性能去实测 + while time.perf_counter() < end_time: + pass + + +def get_colors_in_area(x, y): + colors = set() # 使用集合来存储颜色值,以避免重复 + num_points = AREA_POINT_NUM # 要获取的点的数量 + angle_increment = 360.0 / num_points # 角度增量 + cos_values = [math.cos(math.radians(i * angle_increment)) for i in range(num_points)] + sin_values = [math.sin(math.radians(i * angle_increment)) for i in range(num_points)] + # 处理中心点 + if 0 <= x < exp_image_width and 0 <= y < exp_image_height: + colors.add(get_color_name(exp_image.getpixel((x, y)))) + # 处理圆上的点 + for i in range(num_points): + dx = int(AREA_SCOPE * cos_values[i]) + dy = int(AREA_SCOPE * sin_values[i]) + px = x + dx + py = y + dy + if 0 <= px < exp_image_width and 0 <= py < exp_image_height: + colors.add(get_color_name(exp_image.getpixel((px, py)))) + return list(colors) + + +def get_color_name(pixel): + return str(pixel[0]) + "-" + str(pixel[1]) + "-" + str(pixel[2]) + + +# def convert(touch_data): +# copy_exp_list = copy.deepcopy(exp_list) +# touch_data_values = list(touch_data.values()) +# touch_keys = set() +# touched = 0 +# for i in touch_data_values: +# touched += 1 +# x = i["x"] +# y = i["y"] +# for rgb_str in get_colors_in_area(x, y): +# if not rgb_str in exp_image_dict: +# continue +# touch_keys.add(exp_image_dict[rgb_str]) +# # print("Touched:", touched) +# # print("Touch Keys:", touch_keys) +# touch_keys_list = list(touch_keys) +# for i in range(len(copy_exp_list)): +# for j in range(len(copy_exp_list[i])): +# if copy_exp_list[i][j] in touch_keys_list: +# copy_exp_list[i][j] = 1 +# else: +# copy_exp_list[i][j] = 0 +# # print(copy_exp_list) +# serial_manager.change_touch(copy_exp_list, touch_keys_list) + + +def convert(touch_data): + copy_exp_list = copy.deepcopy(exp_list) + touch_keys = set() + for i in touch_data.values(): + colors = set(get_colors_in_area(i["x"], i["y"])) + touch_keys.update(exp_image_dict[rgb_str] for rgb_str in colors if rgb_str in exp_image_dict) + touch_keys_list = list(touch_keys) + copy_exp_list = [[1 if item in touch_keys_list else 0 for item in sublist] for sublist in copy_exp_list] + serial_manager.change_touch(copy_exp_list, touch_keys_list) + + +def getevent(): + # 存储多点触控数据的列表 + touch_data = {} + + pygame.init() + icon = pygame.image.load(icon_path) + pygame.display.set_icon(icon) + screen_width, screen_height = MONITOR_SIZE + screen = pygame.display.set_mode((screen_width, screen_height)) + pygame.display.set_caption("maimai-windows-touch-panel") + + fuchsia = (128, 128, 128) + hwnd = pygame.display.get_wm_info()["window"] + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, + win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED) + win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 1, win32con.LWA_ALPHA) + screen.fill(fuchsia) # 使用透明背景 + + clock = pygame.time.Clock() + while True: + # start_time = time.perf_counter() + clock.tick(120) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + break + elif event.type == pygame.FINGERDOWN or event.type == pygame.FINGERUP or event.type == pygame.FINGERMOTION: + touch_id = event.finger_id + touch_x, touch_y = event.x * screen_width, event.y * screen_height + if event.type == pygame.FINGERDOWN or event.type == pygame.FINGERMOTION: + touch_data[str(touch_id)] = {} + if not REVERSE_MONITOR: + touch_data[str(touch_id)]["x"] = touch_x + touch_data[str(touch_id)]["y"] = touch_y + else: + touch_data[str(touch_id)]["x"] = MONITOR_SIZE[0] - touch_x + touch_data[str(touch_id)]["y"] = MONITOR_SIZE[1] - touch_y + elif event.type == pygame.FINGERUP: + touch_data.pop(str(touch_id)) + convert(touch_data) + # print("单次执行时间:", (time.perf_counter() - start_time) * 1e3, "毫秒") + + +exp_image = Image.open(IMAGE_PATH) +exp_image_width, exp_image_height = exp_image.size + +if __name__ == "__main__": + os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1" + import pygame + + yaml_file_path = 'config.yaml' + if len(sys.argv) > 1: + yaml_file_path = sys.argv[1] + if os.path.isfile(yaml_file_path): + print("使用配置文件:", yaml_file_path) + with open(yaml_file_path, 'r', encoding='utf-8') as file: + c = yaml.safe_load(file) + IMAGE_PATH = c["IMAGE_PATH"] + COM_PORT = c["COM_PORT"] + COM_BAUDRATE = c["COM_BAUDRATE"] + AREA_SCOPE = c["AREA_SCOPE"] + AREA_POINT_NUM = c["AREA_POINT_NUM"] + MONITOR_SIZE = c["MONITOR_SIZE"] + REVERSE_MONITOR = c["REVERSE_MONITOR"] + TOUCH_THREAD_SLEEP_MODE = c["TOUCH_THREAD_SLEEP_MODE"] + TOUCH_THREAD_SLEEP_DELAY = c["TOUCH_THREAD_SLEEP_DELAY"] + exp_image_dict = c["exp_image_dict"] + else: + print("未找到配置文件, 使用默认配置") + + print(('已' if REVERSE_MONITOR else '未') + "开启屏幕反转") + serial_manager = SerialManager() + serial_manager.start() + threading.Thread(target=getevent).start() + while True: + input_str = input().strip() + if len(input_str) == 0: + continue + if input_str == 'start': + serial_manager.startUp = True + print("已连接到游戏") + elif input_str == 'reverse': + REVERSE_MONITOR = not REVERSE_MONITOR + print("已" + ('开启' if REVERSE_MONITOR else '关闭') + "屏幕反转") + elif input_str == 'restart': + restart_script() + else: + print("未知的输入") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2b5344b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Pillow +pyserial +pyyaml +pygame +pywin32 \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..51020fb --- /dev/null +++ b/start.bat @@ -0,0 +1,3 @@ +@echo off +python .\main.py +pause \ No newline at end of file