Skip to content

Commit 22d2889

Browse files
committed
Merge branch 'master' of code.gitlink.org.cn:mahaotian/DeviceShare
2 parents 4602630 + c81fe26 commit 22d2889

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2516
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/.idea/
2+
**/__pycache__
3+
./devices.json
4+
/dist/
5+
/build/
6+
*.spec
7+
**/*.key
8+
**/*.pyc
9+
**/*.db
10+
.DS_Store

cliptest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import pyperclip
2+
3+
pyperclip.copy("123")
4+
print(pyperclip.paste())

deviceShare.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import sys
2+
3+
4+
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QPushButton, QLabel
5+
import qt_material
6+
from src.sharer.client import Client
7+
from src.sharer.server import Server
8+
9+
10+
class RoleSelectionDialog(QDialog):
11+
def __init__(self):
12+
super().__init__()
13+
14+
self.setFixedSize(400, 200)
15+
16+
self.setWindowTitle("身份选择")
17+
self.layout = QVBoxLayout()
18+
19+
self.server_button = QPushButton("主控机")
20+
self.client_button = QPushButton("被控机")
21+
22+
self.layout.addWidget(self.server_button)
23+
self.layout.addWidget(self.client_button)
24+
25+
self.setLayout(self.layout)
26+
27+
self.server_button.clicked.connect(self.select_server)
28+
self.client_button.clicked.connect(self.select_client)
29+
30+
self.selected_role = None
31+
32+
def select_server(self):
33+
self.selected_role = "server"
34+
self.accept()
35+
36+
def select_client(self):
37+
self.selected_role = "client"
38+
self.accept()
39+
40+
41+
def main():
42+
app = QApplication(sys.argv)
43+
qt_material.apply_stylesheet(app, theme='dark_blue.xml')
44+
selected_role = 'client'
45+
# 显示身份选择弹窗
46+
role_dialog = RoleSelectionDialog()
47+
if role_dialog.exec_() == QDialog.Accepted:
48+
selected_role = role_dialog.selected_role
49+
if selected_role == 'server':
50+
Server(app)
51+
elif selected_role == 'client':
52+
Client(app)
53+
app.exec_()
54+
55+
if __name__ == "__main__":
56+
main()

keyboard_controller.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import threading
2+
import time
3+
4+
from evdev import InputDevice, categorize, ecodes, list_devices, UInput
5+
import os
6+
7+
8+
class KeyboardController:
9+
def __init__(self):
10+
devices = [InputDevice(path) for path in list_devices()]
11+
# 打印所有设备以便调试
12+
# for device in devices:
13+
# print(f"设备名: {device.name}, 设备路径: {device.path}")
14+
# 找到在线的键盘设备
15+
self.stop_event = threading.Event()
16+
17+
self.keyboard_devices = []
18+
for device in devices:
19+
if not os.path.exists(device.path):
20+
continue
21+
capabilities = device.capabilities()
22+
if ecodes.EV_KEY in capabilities and ecodes.EV_SYN in capabilities:
23+
if ecodes.KEY_A in capabilities[ecodes.EV_KEY]: # 检查是否有键盘按键
24+
self.keyboard_devices.append(device)
25+
capabilities = {ecodes.EV_KEY: list(ecodes.keys.keys())}
26+
self.ui = UInput(capabilities, name="virtual_keyboard")
27+
28+
def press(self, key):
29+
self.ui.write(ecodes.EV_KEY, key, 1)
30+
self.ui.syn()
31+
32+
def release(self, key):
33+
self.ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0)
34+
self.ui.syn()
35+
36+
def click(self, click_type, keyData):
37+
key = self.keyFactory.outPut(keyData)
38+
if click_type == 'press':
39+
self.press(key)
40+
elif click_type == 'release':
41+
self.release(key)
42+
43+
def run_keyboard_listener(self, keyboard, on_press, on_release, suppress=False):
44+
if suppress:
45+
keyboard.grab()
46+
try:
47+
while not self.stop_event.is_set():
48+
event = keyboard.read_one() # 非阻塞读取事件
49+
if event:
50+
if event.type == ecodes.EV_KEY:
51+
key_event = categorize(event)
52+
if key_event.keystate == key_event.key_down:
53+
on_press(key_event.keycode)
54+
elif key_event.keystate == key_event.key_up:
55+
on_release(key_event.keycode)
56+
else:
57+
time.sleep(0.01) # 如果没有事件,休眠一段时间,减少 CPU 使用率
58+
except KeyboardInterrupt:
59+
print("Stopped listening for events.")
60+
except Exception as e:
61+
print(f"发生错误: {e}")
62+
finally:
63+
if suppress:
64+
keyboard.ungrab()
65+
66+
def keyboard_listener(self, on_press, on_release, suppress=False):
67+
self.listener = []
68+
for keyboard in self.keyboard_devices:
69+
print(f"监听设备: {keyboard.name} at {keyboard.path}")
70+
self.listener.append(
71+
threading.Thread(target=self.run_keyboard_listener, args=(keyboard, on_press, on_release, suppress)))
72+
for i in self.listener:
73+
i.start()
74+
return self.listener
75+
76+
def stop_listener(self):
77+
self.stop_event.set()
78+
self.listener.clear()
79+
80+
def __del__(self):
81+
self.ui.close()
82+
83+
84+
if __name__ == '__main__':
85+
def on_press(key):
86+
print(f"按下: {key}")
87+
88+
def on_release(key):
89+
print(f"释放: {key}")
90+
91+
keyboardController = KeyboardController()
92+
keyboardController.keyboard_listener(on_press, on_release, suppress=False)
93+
for i in range(10):
94+
keyboardController.press(ecodes.KEY_A)
95+
time.sleep(0.01)
96+
keyboardController.release(ecodes.KEY_A)
97+
time.sleep(1)
98+
keyboardController.stop_listener()

mouse_controller.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import threading
2+
import time
3+
4+
from evdev import InputDevice, categorize, ecodes, list_devices, UInput
5+
import subprocess
6+
7+
8+
class MouseController:
9+
10+
def __init__(self):
11+
devices = [InputDevice(path) for path in list_devices()]
12+
self.stop_event = threading.Event()
13+
self.mouse_devices = []
14+
for device in devices:
15+
capabilities = device.capabilities()
16+
if ecodes.EV_REL in capabilities and ecodes.EV_KEY in capabilities:
17+
if ecodes.REL_X in capabilities[ecodes.EV_REL] and ecodes.REL_Y in capabilities[ecodes.EV_REL]:
18+
if ecodes.BTN_LEFT in capabilities[ecodes.EV_KEY] or ecodes.BTN_RIGHT in capabilities[
19+
ecodes.EV_KEY]:
20+
self.mouse_devices.append(device)
21+
capabilities = {ecodes.EV_REL: [ecodes.REL_X, ecodes.REL_Y, ecodes.REL_WHEEL],
22+
ecodes.EV_KEY: [ecodes.BTN_LEFT, ecodes.BTN_RIGHT, ecodes.BTN_MIDDLE]}
23+
self.ui = UInput(capabilities, name="virtual_mouse")
24+
25+
def update_last_position(self):
26+
pass
27+
28+
def get_last_position(self):
29+
pass
30+
31+
def get_position(self):
32+
result = subprocess.run(['xdotool', 'getmouselocation'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
33+
text=True)
34+
output = result.stdout
35+
# 解析输出
36+
location = {}
37+
for item in output.split():
38+
key, value = item.split(':')
39+
location[key] = int(value)
40+
return location['x'], location['y']
41+
42+
def move_to(self, position: tuple):
43+
pass
44+
45+
def move(self, dx, dy):
46+
pass
47+
48+
def scroll(self, dx, dy):
49+
pass
50+
51+
def click(self, button, pressed):
52+
pass
53+
54+
def run_mouse_listener(self, mouse, on_click, on_move, on_scroll, suppress=False):
55+
if suppress:
56+
mouse.grab()
57+
try:
58+
while not self.stop_event.is_set():
59+
event = mouse.read_one() # 非阻塞读取事件
60+
if event:
61+
if event.type == ecodes.EV_REL:
62+
# if event.code == ecodes.REL_X:
63+
# on_move(event.value, 0)
64+
# elif event.code == ecodes.REL_Y:
65+
# on_move(0, event.value)
66+
# elif event.code == ecodes.REL_WHEEL:
67+
# on_scroll(0, event.value)
68+
print(f"REL: {event}")
69+
elif event.type == ecodes.EV_KEY:
70+
if event.code == ecodes.BTN_LEFT:
71+
if event.value == 1:
72+
on_click('Button.left', 'press')
73+
elif event.value == 0:
74+
on_click('Button.left', 'release')
75+
elif event.code == ecodes.BTN_RIGHT:
76+
if event.value == 1:
77+
on_click('Button.right', 'press')
78+
elif event.value == 0:
79+
on_click('Button.right', 'release')
80+
else:
81+
time.sleep(0.01)
82+
except KeyboardInterrupt:
83+
print("Stopped listening for events.")
84+
except Exception as e:
85+
print(f"发生错误: {e}")
86+
finally:
87+
if suppress:
88+
mouse.ungrab()
89+
90+
def mouse_listener(self, on_click, on_move, on_scroll, suppress=False):
91+
self.listener = []
92+
for mouse in self.mouse_devices:
93+
print(f"监听设备: {mouse.name} at {mouse.path}")
94+
self.listener.append(threading.Thread(target=self.run_mouse_listener,args=(mouse, on_click, on_move, on_scroll, suppress)))
95+
for i in self.listener:
96+
i.start()
97+
return self.listener
98+
99+
def stop_listener(self):
100+
self.stop_event.set()
101+
self.listener.clear()
102+
103+
def __del__(self):
104+
self.ui.close()
105+
106+
107+
if __name__ == '__main__':
108+
109+
# mouseController = MouseController()
110+
# mouseController.mouse_listener(None, None, None, suppress=True)
111+
# for i in range(10):
112+
# mouseController.click('Button.left', 'press')
113+
# time.sleep(0.01)
114+
# mouseController.click('Button.left', 'release')
115+
# time.sleep(1)
116+
# mouseController.stop_listener()
117+
ui = UInput()
118+
def move_mouse(dx,dy):
119+
ui.write(ecodes.EV_REL, ecodes.REL_X, dx)
120+
ui.write(ecodes.EV_REL, ecodes.REL_Y, dy)
121+
ui.syn()
122+
123+
move_mouse(100, 100)
124+
ui.close()

readme.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# DeviceShare —— 跨平台多主机输入设备共享
2+
3+
## 项目介绍
4+
5+
DeviceShare 是一款跨平台的多主机输入设备共享工具,支持 Windows、Linux、MacOS 等操作系统。通过 DeviceShare,您可以在多台主机之间共享鼠标、键盘、剪贴板等输入设备,实现多台主机之间的输入设备共享。
6+
7+
8+
## 功能特性
9+
10+
1. **跨平台支持**:支持 Windows、Linux、MacOS 等操作系统。
11+
2. **多主机支持**:支持多台主机之间的输入设备共享。
12+
3. **鼠标共享**:支持鼠标在多台主机之间的共享。
13+
4. **键盘共享**:支持键盘在多台主机之间的共享。
14+
5. **剪贴板共享**:支持剪贴板在多台主机之间的共享。
15+
6. **屏幕位置配置**:支持配置屏幕位置,方便多台主机之间的切换。
16+
17+
**演示环境**
18+
![1719817611466.png](https://img.qylh.xyz/blog/1719817611466.png)
19+
20+
**双机演示**
21+
22+
https://github.com/qy-liuhuo/deviceShare/assets/60374114/6e126292-22e0-4d91-bab9-272470689ecd
23+
24+
25+
**三机演示**
26+
27+
https://github.com/qy-liuhuo/deviceShare/assets/60374114/1b911b8a-976f-4128-9518-9c64c73a7a39
28+
29+
30+
31+
## 使用说明
32+
针对x86架构的Windows、Kylin、Debian操作系统以及MacOS系统,我们打包构建了可执行程序,可在Github Release界面下载合适的版本。
33+
34+
若构建的版本无法支持目标机器,可选择源码运行或自行打包。该方案需具备Python3 环境,具体步骤如下:
35+
1. 获取项目代码
36+
2. 使用pip install -r requirements.txt命令安装依赖
37+
3. 选择执行run_server.py 或 run_client.py
38+
4. 安装pyinstaller
39+
5. 使用pyinstaller打包目标程序
40+
41+
6. 注意Kylin操作系统在安装python的evdev依赖时可能出现错误,请选择安装预编译版本evdev-binary,参考 https://python-evdev.readthedocs.io/en/latest/install.html
42+
43+
44+
## 系统架构
45+
46+
![1718631831164.png](https://img.qylh.xyz/blog/1718631831164.png)
47+
48+
项目的整体设计框架如上图所示,整体由四个部分构成:
49+
- 服务端为Hid Input设备的拥有者,可向其他客户端主机共享其拥有的输入设备。
50+
- 客户端可使用主机共享的输入设备。
51+
- 网络通信模块用于服务端和客户端的数据传输。
52+
- 设备控制模块用于读取Hid Input设备信息及控制Hid Input设备。
53+
54+
软件的运行流程如下图所示,客户端与服务端作为两个独立模块单独启动,客户端启动后会向局域网中广播自身信息,
55+
服务端收到广播信息后将其加入主机列表,并配置屏幕间的相对位置信息。当服务端主机的光标移出屏幕范围后,会自动判断接下来被控的主机,
56+
并将本机输入设备产生的输入拦截,通过网络模块转发给客户端,客户端收到输入信息后响应相应的控制信号。当客户端的光标移出范围后向服务端主机发送事件标志,
57+
服务端主机停止控制信号的转发,并恢复输入事件的响应。
58+
![1718631845356.png](https://img.qylh.xyz/blog/1718631845356.png)
59+
60+
61+
## TODO
62+
- [x] 主机发现机制
63+
- [x] 屏幕位置配置
64+
- [x] 鼠标共享功能
65+
- [x] 键盘共享功能
66+
- [x] 剪切板共享功能
67+
- [ ] 文件拖拽共享功能
68+
- [ ] 剪切板内容加密传输
69+
- [ ] 解耦各设备共享模块,支持用户自定义开关相关功能
70+
- [ ] 优化屏幕管理功能
71+
- [ ] 优化代码质量,提升代码可读性,提升软件性能和稳定性
72+
- [ ] 测试更多类型操作系统
73+
- [ ] 优化文档

requirements.txt

592 Bytes
Binary file not shown.

resources/background.jpg

16.4 KB
Loading

resources/background1.jpg

13.4 KB
Loading

resources/devicelink.ico

2.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)