initial commit
|
@ -0,0 +1,7 @@
|
||||||
|
/.idea
|
||||||
|
/__pycache__
|
||||||
|
/start_test.bat
|
||||||
|
/start_wec.bat
|
||||||
|
/config_wec.yaml
|
||||||
|
/config_zgh.yaml
|
||||||
|
/start_zgh.bat
|
|
@ -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.
|
|
@ -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)
|
||||||
|
|
||||||
|
## 其他
|
||||||
|
|
||||||
|
编辑好的区块成品图类似这样:
|
||||||
|
|
||||||
|

|
|
@ -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
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 2.8 MiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 380 KiB |
After Width: | Height: | Size: 369 KiB |
|
@ -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("未知的输入")
|
|
@ -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("未知的输入")
|
|
@ -0,0 +1,5 @@
|
||||||
|
Pillow
|
||||||
|
pyserial
|
||||||
|
pyyaml
|
||||||
|
pygame
|
||||||
|
pywin32
|