pych9329 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pych9329-0.1.5/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wevsty
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.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.3
2
+ Name: pych9329
3
+ Version: 0.1.5
4
+ Summary: Python module to control the ch9329
5
+ License: MIT
6
+ Author: wevsty
7
+ Author-email: ty@wevs.org
8
+ Requires-Python: >3.10
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: pyserial (>=3.5,<4.0)
15
+ Project-URL: Repository, https://github.com/wevsty/pych9329
16
+ Description-Content-Type: text/markdown
17
+
18
+ # pych9329
19
+ Python module to control ch9329
20
+
21
+ ## Installation
22
+
23
+ You can install the package via pip:
24
+
25
+ ```bash
26
+ pip install pych9329
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```py
32
+ from serial import Serial
33
+
34
+ from pych9329 import keyboard
35
+ from pych9329 import mouse
36
+ from pych9329.chip_command import get_manufacturer
37
+ from pych9329.chip_command import get_product
38
+ from pych9329.chip_command import get_serial_number
39
+
40
+
41
+ def main():
42
+ ser = Serial("COM10", 9600, timeout=0.50)
43
+
44
+ keyboard.click(ser, "a")
45
+ keyboard.click(ser, "A")
46
+ keyboard.send_text_data(ser, "\n")
47
+ keyboard.send_text_data(ser, "Hello World\n")
48
+ keyboard.send_text_data(ser, "abcdefghijklmnopqrstuvwxyz\n")
49
+ keyboard.send_text_data(ser, "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n")
50
+ keyboard.send_text_data(ser, "0123456789\n")
51
+ keyboard.send_text_data(ser, "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\n")
52
+
53
+ mouse.move(ser, x=500, y=500)
54
+ mouse.move(ser, x=50, y=50, relative_mode=True)
55
+ # For supported key names, please see mouse.MOUSE_BUTTON_NAME_MAP
56
+ mouse.click(ser, button_name="left")
57
+
58
+ # 2019A152BB40
59
+ print(get_serial_number(ser))
60
+ # WCH UART TO KB-MS_V1.8
61
+ print(get_product(ser))
62
+ # WWW.WCH.CN
63
+ print(get_manufacturer(ser))
64
+ ser.close()
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
69
+
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT license.
75
+
76
+ ## Contributing
77
+
78
+ This project is modified from ch9329 library.
79
+
80
+ Thanks to the original author of the ch9329 library [Pradish Bijukchhe](https://github.com/pradishb)
81
+
@@ -0,0 +1,63 @@
1
+ # pych9329
2
+ Python module to control ch9329
3
+
4
+ ## Installation
5
+
6
+ You can install the package via pip:
7
+
8
+ ```bash
9
+ pip install pych9329
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```py
15
+ from serial import Serial
16
+
17
+ from pych9329 import keyboard
18
+ from pych9329 import mouse
19
+ from pych9329.chip_command import get_manufacturer
20
+ from pych9329.chip_command import get_product
21
+ from pych9329.chip_command import get_serial_number
22
+
23
+
24
+ def main():
25
+ ser = Serial("COM10", 9600, timeout=0.50)
26
+
27
+ keyboard.click(ser, "a")
28
+ keyboard.click(ser, "A")
29
+ keyboard.send_text_data(ser, "\n")
30
+ keyboard.send_text_data(ser, "Hello World\n")
31
+ keyboard.send_text_data(ser, "abcdefghijklmnopqrstuvwxyz\n")
32
+ keyboard.send_text_data(ser, "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n")
33
+ keyboard.send_text_data(ser, "0123456789\n")
34
+ keyboard.send_text_data(ser, "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\n")
35
+
36
+ mouse.move(ser, x=500, y=500)
37
+ mouse.move(ser, x=50, y=50, relative_mode=True)
38
+ # For supported key names, please see mouse.MOUSE_BUTTON_NAME_MAP
39
+ mouse.click(ser, button_name="left")
40
+
41
+ # 2019A152BB40
42
+ print(get_serial_number(ser))
43
+ # WCH UART TO KB-MS_V1.8
44
+ print(get_product(ser))
45
+ # WWW.WCH.CN
46
+ print(get_manufacturer(ser))
47
+ ser.close()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
52
+
53
+ ```
54
+
55
+ ## License
56
+
57
+ MIT license.
58
+
59
+ ## Contributing
60
+
61
+ This project is modified from ch9329 library.
62
+
63
+ Thanks to the original author of the ch9329 library [Pradish Bijukchhe](https://github.com/pradishb)
@@ -0,0 +1,23 @@
1
+ [tool.poetry]
2
+ name = "pych9329"
3
+ version = "0.1.5"
4
+ description = "Python module to control the ch9329"
5
+ authors = ["wevsty <ty@wevs.org>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/wevsty/pych9329"
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">3.10"
12
+ pyserial = "^3.5"
13
+
14
+ [tool.poetry.group.dev.dependencies]
15
+ black = "^25.1.0"
16
+ isort = "^6.0.1"
17
+ pytest = "^8.3.5"
18
+ pytest-mock = "^3.14.0"
19
+ pytest-datadir = "^1.6.1"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,200 @@
1
+ from enum import Enum
2
+
3
+ from serial import Serial
4
+
5
+ import pych9329.exceptions as exceptions
6
+ import pych9329.frame_utils as frame_utils
7
+ from pych9329.struct_utils import Structure
8
+
9
+
10
+ class ChipParameter(Structure):
11
+ _fields_ = [
12
+ ('c', 'work_mode'),
13
+ ('c', 'serial_communication_mode'),
14
+ ('c', 'serial_address'),
15
+ ('>I', 'serial_baud_rate'),
16
+ ('>H', 'reserve_00'),
17
+ ('>H', 'communication_packet_interval'),
18
+ ('<H', 'usb_vid'),
19
+ ('<H', 'usb_pid'),
20
+ ('>H', 'keyboard_upload_interval'),
21
+ ('>H', 'keyboard_release_delay'),
22
+ ('>H', 'keyboard_auto_enter_flag'),
23
+ ('4s', 'keyboard_enter_data_1'),
24
+ ('4s', 'keyboard_enter_data_2'),
25
+ ('4s', 'keyboard_filtering_start'),
26
+ ('c', 'usb_string_descriptor_flag'),
27
+ ('4s', 'keyboard_fast_upload_flag'),
28
+ ('12s', 'reserve_01'),
29
+ ]
30
+
31
+
32
+ def get_chip_parameters(serial_object: Serial) -> ChipParameter:
33
+ # request frame
34
+ data_frame = frame_utils.DataFrame(
35
+ b'\x57\xab',
36
+ b'\x00',
37
+ b'\x08',
38
+ b'\x00',
39
+ b'',
40
+ )
41
+ request_packet = data_frame.create_frame()
42
+ serial_object.write(request_packet)
43
+ serial_object.flush()
44
+ # Reply frame should be 56 bytes
45
+ reply_length = 56
46
+ reply_buffer = serial_object.read(reply_length)
47
+ result = data_frame.parse_frame(reply_buffer)
48
+ if result is False:
49
+ raise exceptions.ProtocolError('parser buffer error')
50
+ chip_parameter = ChipParameter.from_buffer(data_frame.get_data())
51
+ return chip_parameter
52
+
53
+
54
+ def set_chip_parameters(serial_object: Serial, parameter: ChipParameter) -> bool:
55
+ parameter.buffer_flush()
56
+ parameter_buffer = parameter.buffer
57
+ # request frame
58
+ data_frame = frame_utils.DataFrame(
59
+ b'\x57\xab',
60
+ b'\x00',
61
+ b'\x09',
62
+ b'\x00',
63
+ b'\x00',
64
+ )
65
+ data_frame.set_data(parameter_buffer)
66
+ request_packet = data_frame.create_frame()
67
+ serial_object.write(request_packet)
68
+ serial_object.flush()
69
+ # Reply frame should be 7 bytes
70
+ reply_length = 7
71
+ reply_buffer = serial_object.read(reply_length)
72
+ result = data_frame.parse_frame(reply_buffer)
73
+ if result is False:
74
+ raise exceptions.ProtocolError('parser buffer error')
75
+ reply_data = data_frame.get_data()
76
+ if reply_data == frame_utils.DataFrameStatus.SUCCESS.value:
77
+ return True
78
+ return False
79
+
80
+
81
+ class USBStringSubCommand(Enum):
82
+ MANUFACTURER = b'\x00'
83
+ PRODUCT = b'\x01'
84
+ SERIAL_NUMBER = b'\x02'
85
+
86
+
87
+ def get_usb_string_info(serial_object: Serial, sub_command: bytes) -> str:
88
+ data_frame = frame_utils.DataFrame(
89
+ b'\x57\xab',
90
+ b'\x00',
91
+ b'\x0a',
92
+ b'\x01',
93
+ sub_command,
94
+ )
95
+ request_packet = data_frame.create_frame()
96
+ serial_object.readall()
97
+ serial_object.write(request_packet)
98
+ serial_object.flush()
99
+ reply_buffer = serial_object.readall()
100
+ result = data_frame.parse_frame(reply_buffer)
101
+ if result is False:
102
+ raise exceptions.ProtocolError('parser buffer error')
103
+ if data_frame.get_data_length() < 2:
104
+ raise exceptions.ProtocolError('parser buffer error')
105
+ data_buffer = data_frame.get_data()
106
+ # string_length = data_buffer[1]
107
+ string_bytes = data_buffer[2:]
108
+ # usb_info = string_bytes.decode('utf-8', 'ignore')
109
+ try:
110
+ usb_info = string_bytes.decode('utf-8')
111
+ except UnicodeDecodeError:
112
+ raise exceptions.InvalidStringEncoding('unable decode string')
113
+ return usb_info
114
+
115
+
116
+ def set_usb_string_info(
117
+ serial_object: Serial, sub_command: bytes, string_info: str
118
+ ) -> bool:
119
+ info_data = string_info.encode('utf-8')
120
+ if len(info_data) > 23:
121
+ raise InvalidStringLength('string too long')
122
+ data_buffer = (
123
+ sub_command + int.to_bytes(len(info_data), byteorder='big') + info_data
124
+ )
125
+ data_frame = frame_utils.DataFrame(
126
+ b'\x57\xab',
127
+ b'\x00',
128
+ b'\x0b',
129
+ b'',
130
+ b'',
131
+ )
132
+ data_frame.set_data(data_buffer)
133
+ request_packet = data_frame.create_frame()
134
+ serial_object.readall()
135
+ serial_object.write(request_packet)
136
+ serial_object.flush()
137
+ reply_buffer = serial_object.readall()
138
+ result = data_frame.parse_frame(reply_buffer)
139
+ if result is False:
140
+ raise exceptions.ProtocolError('parser buffer error')
141
+ reply_data = data_frame.get_data()
142
+ if reply_data == frame_utils.DataFrameStatus.SUCCESS.value:
143
+ return True
144
+ return False
145
+
146
+
147
+ def get_serial_number(serial_object: Serial) -> str:
148
+ return get_usb_string_info(serial_object, USBStringSubCommand.SERIAL_NUMBER.value)
149
+
150
+
151
+ def set_serial_number(serial_object: Serial, string_data: str):
152
+ return set_usb_string_info(
153
+ serial_object, USBStringSubCommand.SERIAL_NUMBER.value, string_data
154
+ )
155
+
156
+
157
+ def get_manufacturer(serial_object: Serial) -> str:
158
+ return get_usb_string_info(serial_object, USBStringSubCommand.MANUFACTURER.value)
159
+
160
+
161
+ def set_manufacturer(serial_object: Serial, string_data: str):
162
+ return set_usb_string_info(
163
+ serial_object, USBStringSubCommand.MANUFACTURER.value, string_data
164
+ )
165
+
166
+
167
+ def get_product(serial_object: Serial) -> str:
168
+ return get_usb_string_info(serial_object, USBStringSubCommand.PRODUCT.value)
169
+
170
+
171
+ def set_product(serial_object: Serial, string_data: str):
172
+ return set_usb_string_info(
173
+ serial_object, USBStringSubCommand.PRODUCT.value, string_data
174
+ )
175
+
176
+
177
+ def send_command_reset(serial_object: Serial) -> None:
178
+ data_frame = frame_utils.DataFrame(
179
+ b'\x57\xab',
180
+ b'\x00',
181
+ b'\x0f',
182
+ b'\x00',
183
+ b'',
184
+ )
185
+ request_packet = data_frame.create_frame()
186
+ serial_object.write(request_packet)
187
+
188
+ def send_command_restore_factory_config(serial_object: Serial) -> None:
189
+ data_frame = frame_utils.DataFrame(
190
+ b'\x57\xab',
191
+ b'\x00',
192
+ b'\x0c',
193
+ b'\x00',
194
+ b'',
195
+ )
196
+ request_packet = data_frame.create_frame()
197
+ serial_object.write(request_packet)
198
+
199
+ if __name__ == '__main__':
200
+ pass
@@ -0,0 +1,50 @@
1
+ class ChipBaseException(Exception):
2
+ pass
3
+
4
+
5
+ # 协议错误
6
+ class ProtocolError(ChipBaseException):
7
+ pass
8
+
9
+
10
+ # 无效的字符串编码
11
+ class InvalidStringEncoding(ChipBaseException):
12
+ pass
13
+
14
+
15
+ # 无效的字符串长度
16
+ class InvalidStringLength(ChipBaseException):
17
+ pass
18
+
19
+
20
+ # 无效的坐标值
21
+ class InvalidCoordinateValue(ChipBaseException):
22
+ pass
23
+
24
+
25
+ # 无效的滚轮值
26
+ class InvalidWheelValue(ChipBaseException):
27
+ pass
28
+
29
+
30
+ # 无效的特殊按键
31
+ class InvalidModifierKey(ChipBaseException):
32
+ pass
33
+
34
+
35
+ # 无效的按键
36
+ class InvalidKey(ChipBaseException):
37
+ pass
38
+
39
+
40
+ # 传入太多按键
41
+ class TooManyKeys(ChipBaseException):
42
+ pass
43
+
44
+
45
+ # 缓冲区太小
46
+ class BufferTooSmall(ChipBaseException):
47
+ pass
48
+
49
+ if __name__ == '__main__':
50
+ pass
@@ -0,0 +1,99 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+
5
+ class DataFrameStatus(Enum):
6
+ SUCCESS = b'\x00'
7
+ ERROR_TIMEOUT = b'\xe1'
8
+ ERROR_HEAD = b'\xe2'
9
+ ERROR_CMD = b'\xe3'
10
+ ERROR_SUM = b'\xe4'
11
+ ERROR_PARA = b'\xe5'
12
+ ERROR_OPERATE = b'\xe6'
13
+
14
+
15
+ @dataclass
16
+ class DataFrame:
17
+ HEAD: bytes
18
+ ADDR: bytes
19
+ CMD: bytes
20
+ LEN: bytes = b'\x00'
21
+ DATA: bytes = b''
22
+
23
+ def create_frame(self) -> bytes:
24
+ checksum = self.calc_checksum()
25
+ frame = (
26
+ self.HEAD + self.ADDR + self.CMD + self.LEN + self.DATA + bytes([checksum])
27
+ )
28
+ return frame
29
+
30
+ def get_data(self) -> bytes:
31
+ return self.DATA
32
+
33
+ def get_data_length(self) -> int:
34
+ length = int.from_bytes(self.LEN, byteorder='big')
35
+ return length
36
+
37
+ def set_data(self, data: bytes) -> None:
38
+ self.DATA = data
39
+ self.set_data_length(len(data))
40
+
41
+ def set_data_length(self, length: int) -> None:
42
+ self.LEN = int.to_bytes(length, byteorder='big')
43
+
44
+ def calc_frame_length(self, buffer: bytes) -> int:
45
+ header_size = 5
46
+ frame_length = 0
47
+ buffer_length = len(buffer)
48
+ if buffer_length < header_size:
49
+ return frame_length
50
+ head_first = buffer[0:1]
51
+ head_last = buffer[1:2]
52
+ self.ADDR = buffer[2:3]
53
+ self.CMD = buffer[3:4]
54
+ self.LEN = buffer[4:5]
55
+ self.HEAD = head_first + head_last
56
+ data_length = self.get_data_length()
57
+ frame_length = header_size + data_length + 1
58
+ return frame_length
59
+
60
+ def calc_checksum(self) -> int:
61
+ head_list = list(self.HEAD)
62
+ head_sum = sum(head_list)
63
+ data_list = list(self.DATA)
64
+ data_sum = sum(data_list)
65
+ checksum = (
66
+ sum(
67
+ [
68
+ head_sum,
69
+ int.from_bytes(self.ADDR, byteorder='big'),
70
+ int.from_bytes(self.CMD, byteorder='big'),
71
+ int.from_bytes(self.LEN, byteorder='big'),
72
+ data_sum,
73
+ ]
74
+ )
75
+ % 256
76
+ )
77
+ return checksum
78
+
79
+ def parse_frame(self, buffer: bytes) -> bool:
80
+ header_size = 5
81
+ parse_result = False
82
+ buffer_length = len(buffer)
83
+ if buffer_length < header_size:
84
+ return parse_result
85
+ frame_length = self.calc_frame_length(buffer)
86
+ if buffer_length < frame_length:
87
+ return parse_result
88
+ data_length = self.get_data_length()
89
+ if data_length >= 0:
90
+ self.DATA = buffer[header_size : header_size + data_length]
91
+ _checksum = buffer[
92
+ header_size + data_length : header_size + data_length + 1
93
+ ]
94
+ parse_result = True
95
+ return parse_result
96
+
97
+
98
+ if __name__ == '__main__':
99
+ pass
@@ -0,0 +1,190 @@
1
+ import pych9329.hid_code_table as hid_code
2
+
3
+ HID_CODE_MAP = {
4
+ '': (hid_code.KEY_NONE, False),
5
+ # release alias
6
+ 'null': (hid_code.KEY_NONE, False),
7
+ 'none': (hid_code.KEY_NONE, False),
8
+ 'a': (hid_code.KEY_A, False),
9
+ 'b': (hid_code.KEY_B, False),
10
+ 'c': (hid_code.KEY_C, False),
11
+ 'd': (hid_code.KEY_D, False),
12
+ 'e': (hid_code.KEY_E, False),
13
+ 'f': (hid_code.KEY_F, False),
14
+ 'g': (hid_code.KEY_G, False),
15
+ 'h': (hid_code.KEY_H, False),
16
+ 'i': (hid_code.KEY_I, False),
17
+ 'j': (hid_code.KEY_J, False),
18
+ 'k': (hid_code.KEY_K, False),
19
+ 'l': (hid_code.KEY_L, False),
20
+ 'm': (hid_code.KEY_M, False),
21
+ 'n': (hid_code.KEY_N, False),
22
+ 'o': (hid_code.KEY_O, False),
23
+ 'p': (hid_code.KEY_P, False),
24
+ 'q': (hid_code.KEY_Q, False),
25
+ 'r': (hid_code.KEY_R, False),
26
+ 's': (hid_code.KEY_S, False),
27
+ 't': (hid_code.KEY_T, False),
28
+ 'u': (hid_code.KEY_U, False),
29
+ 'v': (hid_code.KEY_V, False),
30
+ 'w': (hid_code.KEY_W, False),
31
+ 'x': (hid_code.KEY_X, False),
32
+ 'y': (hid_code.KEY_Y, False),
33
+ 'z': (hid_code.KEY_Z, False),
34
+ '1': (hid_code.KEY_1, False),
35
+ '2': (hid_code.KEY_2, False),
36
+ '3': (hid_code.KEY_3, False),
37
+ '4': (hid_code.KEY_4, False),
38
+ '5': (hid_code.KEY_5, False),
39
+ '6': (hid_code.KEY_6, False),
40
+ '7': (hid_code.KEY_7, False),
41
+ '8': (hid_code.KEY_8, False),
42
+ '9': (hid_code.KEY_9, False),
43
+ '0': (hid_code.KEY_0, False),
44
+ 'enter': (hid_code.KEY_ENTER, False),
45
+ 'esc': (hid_code.KEY_ESCAPE, False),
46
+ 'escape': (hid_code.KEY_ESCAPE, False),
47
+ 'backspace': (hid_code.KEY_BACKSPACE, False),
48
+ 'tab': (hid_code.KEY_TAB, False),
49
+ 'space': (hid_code.KEY_SPACE, False),
50
+ 'bracket_left': (hid_code.KEY_BRACKET_LEFT, False),
51
+ 'bracket_right': (hid_code.KEY_BRACKET_RIGHT, False),
52
+ 'backslash': (hid_code.KEY_CODE_29, False),
53
+ # 'europe_1': (hid_code.KEY_EUROPE_1, False),
54
+ 'semicolon': (hid_code.KEY_SEMICOLON, False),
55
+ 'apostrophe': (hid_code.KEY_APOSTROPHE, False),
56
+ 'grave': (hid_code.KEY_GRAVE, False),
57
+ 'comma': (hid_code.KEY_COMMA, False),
58
+ 'period': (hid_code.KEY_PERIOD, False),
59
+ 'slash': (hid_code.KEY_SLASH, False),
60
+ 'caps_lock': (hid_code.KEY_CAPS_LOCK, False),
61
+ 'f1': (hid_code.KEY_F1, False),
62
+ 'f2': (hid_code.KEY_F2, False),
63
+ 'f3': (hid_code.KEY_F3, False),
64
+ 'f4': (hid_code.KEY_F4, False),
65
+ 'f5': (hid_code.KEY_F5, False),
66
+ 'f6': (hid_code.KEY_F6, False),
67
+ 'f7': (hid_code.KEY_F7, False),
68
+ 'f8': (hid_code.KEY_F8, False),
69
+ 'f9': (hid_code.KEY_F9, False),
70
+ 'f10': (hid_code.KEY_F10, False),
71
+ 'f11': (hid_code.KEY_F11, False),
72
+ 'f12': (hid_code.KEY_F12, False),
73
+ 'print_screen': (hid_code.KEY_PRINT_SCREEN, False),
74
+ 'scroll_lock': (hid_code.KEY_SCROLL_LOCK, False),
75
+ 'pause': (hid_code.KEY_PAUSE, False),
76
+ 'insert': (hid_code.KEY_INSERT, False),
77
+ 'home': (hid_code.KEY_HOME, False),
78
+ 'page_up': (hid_code.KEY_PAGE_UP, False),
79
+ 'delete': (hid_code.KEY_DELETE, False),
80
+ 'del': (hid_code.KEY_DELETE, False),
81
+ 'end': (hid_code.KEY_END, False),
82
+ 'page_down': (hid_code.KEY_PAGE_DOWN, False),
83
+ 'arrow_right': (hid_code.KEY_ARROW_RIGHT, False),
84
+ 'arrow_left': (hid_code.KEY_ARROW_LEFT, False),
85
+ 'arrow_down': (hid_code.KEY_ARROW_DOWN, False),
86
+ 'arrow_up': (hid_code.KEY_ARROW_UP, False),
87
+ 'right': (hid_code.KEY_ARROW_RIGHT, False),
88
+ 'left': (hid_code.KEY_ARROW_LEFT, False),
89
+ 'down': (hid_code.KEY_ARROW_DOWN, False),
90
+ 'up': (hid_code.KEY_ARROW_UP, False),
91
+ 'num_lock': (hid_code.KEY_NUM_LOCK, False),
92
+ 'keypad_divide': (hid_code.KEY_KEYPAD_DIVIDE, False),
93
+ 'keypad_multiply': (hid_code.KEY_KEYPAD_MULTIPLY, False),
94
+ 'keypad_subtract': (hid_code.KEY_KEYPAD_SUBTRACT, False),
95
+ 'keypad_add': (hid_code.KEY_KEYPAD_ADD, False),
96
+ 'keypad_enter': (hid_code.KEY_KEYPAD_ENTER, False),
97
+ 'keypad_1': (hid_code.KEY_KEYPAD_1, False),
98
+ 'keypad_2': (hid_code.KEY_KEYPAD_2, False),
99
+ 'keypad_3': (hid_code.KEY_KEYPAD_3, False),
100
+ 'keypad_4': (hid_code.KEY_KEYPAD_4, False),
101
+ 'keypad_5': (hid_code.KEY_KEYPAD_5, False),
102
+ 'keypad_6': (hid_code.KEY_KEYPAD_6, False),
103
+ 'keypad_7': (hid_code.KEY_KEYPAD_7, False),
104
+ 'keypad_8': (hid_code.KEY_KEYPAD_8, False),
105
+ 'keypad_9': (hid_code.KEY_KEYPAD_9, False),
106
+ 'keypad_0': (hid_code.KEY_KEYPAD_0, False),
107
+ 'keypad_decimal': (hid_code.KEY_KEYPAD_DECIMAL, False),
108
+ # modifiers
109
+ 'ctrl': (hid_code.KEY_CTRL_L, False),
110
+ 'ctrl_left': (hid_code.KEY_CTRL_L, False),
111
+ 'ctrl_right': (hid_code.KEY_CTRL_R, False),
112
+ 'shift': (hid_code.KEY_SHIFT_L, False),
113
+ 'shift_left': (hid_code.KEY_SHIFT_L, False),
114
+ 'shift_right': (hid_code.KEY_SHIFT_R, False),
115
+ 'alt': (hid_code.KEY_ALT_L, False),
116
+ 'alt_left': (hid_code.KEY_ALT_L, False),
117
+ 'alt_right': (hid_code.KEY_ALT_R, False),
118
+ 'win': (hid_code.KEY_WIN_L, False),
119
+ 'win_left': (hid_code.KEY_WIN_L, False),
120
+ 'win_right': (hid_code.KEY_WIN_R, False),
121
+ 'win_app': (hid_code.KEY_WIN_APP, False),
122
+ 'gui': (hid_code.KEY_WIN_L, False),
123
+ 'gui_left': (hid_code.KEY_WIN_L, False),
124
+ 'gui_right': (hid_code.KEY_WIN_R, False),
125
+ # white spaces
126
+ ' ': (hid_code.KEY_SPACE, False),
127
+ '\t': (hid_code.KEY_TAB, False),
128
+ '\n': (hid_code.KEY_ENTER, False),
129
+ # symbols
130
+ '-': (hid_code.KEY_MINUS, False),
131
+ '=': (hid_code.KEY_EQUAL, False),
132
+ '[': (hid_code.KEY_BRACKET_LEFT, False),
133
+ ']': (hid_code.KEY_BRACKET_RIGHT, False),
134
+ '\\': (hid_code.KEY_CODE_29, False),
135
+ ';': (hid_code.KEY_SEMICOLON, False),
136
+ '\'': (hid_code.KEY_APOSTROPHE, False),
137
+ '`': (hid_code.KEY_GRAVE, False),
138
+ ',': (hid_code.KEY_COMMA, False),
139
+ '.': (hid_code.KEY_PERIOD, False),
140
+ '/': (hid_code.KEY_SLASH, False),
141
+ # symbols (shift)
142
+ '^': (hid_code.KEY_6, True),
143
+ '_': (hid_code.KEY_MINUS, True),
144
+ '{': (hid_code.KEY_BRACKET_LEFT, True),
145
+ '|': (hid_code.KEY_CODE_29, True),
146
+ '}': (hid_code.KEY_BRACKET_RIGHT, True),
147
+ '~': (hid_code.KEY_GRAVE, True),
148
+ '!': (hid_code.KEY_1, True),
149
+ '\"': (hid_code.KEY_APOSTROPHE, True),
150
+ '#': (hid_code.KEY_3, True),
151
+ '$': (hid_code.KEY_4, True),
152
+ '%': (hid_code.KEY_5, True),
153
+ '&': (hid_code.KEY_7, True),
154
+ '(': (hid_code.KEY_9, True),
155
+ ')': (hid_code.KEY_0, True),
156
+ '*': (hid_code.KEY_8, True),
157
+ '+': (hid_code.KEY_EQUAL, True),
158
+ ':': (hid_code.KEY_SEMICOLON, True),
159
+ '<': (hid_code.KEY_COMMA, True),
160
+ '>': (hid_code.KEY_PERIOD, True),
161
+ '?': (hid_code.KEY_SLASH, True),
162
+ '@': (hid_code.KEY_2, True),
163
+ # shift keys
164
+ 'A': (hid_code.KEY_A, True),
165
+ 'B': (hid_code.KEY_B, True),
166
+ 'C': (hid_code.KEY_C, True),
167
+ 'D': (hid_code.KEY_D, True),
168
+ 'E': (hid_code.KEY_E, True),
169
+ 'F': (hid_code.KEY_F, True),
170
+ 'G': (hid_code.KEY_G, True),
171
+ 'H': (hid_code.KEY_H, True),
172
+ 'I': (hid_code.KEY_I, True),
173
+ 'J': (hid_code.KEY_J, True),
174
+ 'K': (hid_code.KEY_K, True),
175
+ 'L': (hid_code.KEY_L, True),
176
+ 'M': (hid_code.KEY_M, True),
177
+ 'N': (hid_code.KEY_N, True),
178
+ 'O': (hid_code.KEY_O, True),
179
+ 'P': (hid_code.KEY_P, True),
180
+ 'Q': (hid_code.KEY_Q, True),
181
+ 'R': (hid_code.KEY_R, True),
182
+ 'S': (hid_code.KEY_S, True),
183
+ 'T': (hid_code.KEY_T, True),
184
+ 'U': (hid_code.KEY_U, True),
185
+ 'V': (hid_code.KEY_V, True),
186
+ 'W': (hid_code.KEY_W, True),
187
+ 'X': (hid_code.KEY_X, True),
188
+ 'Y': (hid_code.KEY_Y, True),
189
+ 'Z': (hid_code.KEY_Z, True),
190
+ }
@@ -0,0 +1,161 @@
1
+ KEY_GRAVE = b'\x35'
2
+ KEY_1 = b'\x1e'
3
+ KEY_2 = b'\x1f'
4
+ KEY_3 = b'\x20'
5
+ KEY_4 = b'\x21'
6
+ KEY_5 = b'\x22'
7
+ KEY_6 = b'\x23'
8
+ KEY_7 = b'\x24'
9
+ KEY_8 = b'\x25'
10
+ KEY_9 = b'\x26'
11
+ KEY_0 = b'\x27'
12
+ KEY_MINUS = b'\x2d'
13
+ KEY_EQUAL = b'\x2e'
14
+ KEY_CODE_14 = b'\x89'
15
+ KEY_BACKSPACE = b'\x2a'
16
+ KEY_TAB = b'\x2b'
17
+ KEY_Q = b'\x14'
18
+ KEY_W = b'\x1a'
19
+ KEY_E = b'\x08'
20
+ KEY_R = b'\x15'
21
+ KEY_T = b'\x17'
22
+ KEY_Y = b'\x1c'
23
+ KEY_U = b'\x18'
24
+ KEY_I = b'\x0c'
25
+ KEY_O = b'\x12'
26
+ KEY_P = b'\x13'
27
+ KEY_BRACKET_LEFT = b'\x2f'
28
+ KEY_BRACKET_RIGHT = b'\x30'
29
+ KEY_CODE_29 = b'\x31' # backslash
30
+ KEY_CAPS_LOCK = b'\x39'
31
+ KEY_A = b'\x04'
32
+ KEY_S = b'\x16'
33
+ KEY_D = b'\x07'
34
+ KEY_F = b'\x09'
35
+ KEY_G = b'\x0a'
36
+ KEY_H = b'\x0b'
37
+ KEY_J = b'\x0d'
38
+ KEY_K = b'\x0e'
39
+ KEY_L = b'\x0f'
40
+ KEY_SEMICOLON = b'\x33'
41
+ KEY_APOSTROPHE = b'\x34'
42
+ KEY_CODE_42 = b'\x32'
43
+ KEY_ENTER = b'\x28'
44
+ KEY_SHIFT_L = b'\xe1'
45
+ KEY_CODE_45 = b'\x64'
46
+ KEY_Z = b'\x1d'
47
+ KEY_X = b'\x1b'
48
+ KEY_C = b'\x06'
49
+ KEY_V = b'\x19'
50
+ KEY_B = b'\x05'
51
+ KEY_N = b'\x11'
52
+ KEY_M = b'\x10'
53
+ KEY_COMMA = b'\x36'
54
+ KEY_PERIOD = b'\x37'
55
+ KEY_SLASH = b'\x38'
56
+ KEY_CODE_56 = b'\x87' # Unknown
57
+ KEY_SHIFT_R = b'\xe5'
58
+ KEY_CTRL_L = b'\xe0'
59
+ #
60
+ KEY_ALT_L = b'\xe2'
61
+ KEY_SPACE = b'\x2c'
62
+ KEY_ALT_R = b'\xe6'
63
+ #
64
+ KEY_CTRL_R = b'\xe4'
65
+ #
66
+ #
67
+ #
68
+ #
69
+ #
70
+ #
71
+ #
72
+ #
73
+ #
74
+ #
75
+ KEY_INSERT = b'\x49'
76
+ KEY_DELETE = b'\x4c'
77
+ #
78
+ #
79
+ KEY_ARROW_LEFT = b'\x50'
80
+ KEY_HOME = b'\x4a'
81
+ KEY_END = b'\x4d'
82
+ #
83
+ KEY_ARROW_UP = b'\x52'
84
+ KEY_ARROW_DOWN = b'\x51'
85
+ KEY_PAGE_UP = b'\x4b'
86
+ KEY_PAGE_DOWN = b'\x4e'
87
+ #
88
+ #
89
+ KEY_ARROW_RIGHT = b'\x4f'
90
+ KEY_NUM_LOCK = b'\x53'
91
+ KEY_KEYPAD_7 = b'\x5f'
92
+ KEY_KEYPAD_4 = b'\x5c'
93
+ KEY_KEYPAD_1 = b'\x59'
94
+ #
95
+ KEY_KEYPAD_DIVIDE = b'\x54'
96
+ KEY_KEYPAD_8 = b'\x60'
97
+ KEY_KEYPAD_5 = b'\x5d'
98
+ KEY_KEYPAD_2 = b'\x5a'
99
+ KEY_KEYPAD_0 = b'\x62'
100
+ KEY_KEYPAD_MULTIPLY = b'\x55'
101
+ KEY_KEYPAD_9 = b'\x61'
102
+ KEY_KEYPAD_6 = b'\x5e'
103
+ KEY_KEYPAD_3 = b'\x5b'
104
+ KEY_KEYPAD_DECIMAL = b'\x63'
105
+ KEY_KEYPAD_SUBTRACT = b'\x56'
106
+ KEY_KEYPAD_ADD = b'\x57'
107
+ KEY_CODE_107 = b'\x85' # Unknown
108
+ KEY_KEYPAD_ENTER = b'\x58'
109
+ #
110
+ KEY_ESCAPE = b'\x29'
111
+ #
112
+ KEY_F1 = b'\x3a'
113
+ KEY_F2 = b'\x3b'
114
+ KEY_F3 = b'\x3c'
115
+ KEY_F4 = b'\x3d'
116
+ KEY_F5 = b'\x3e'
117
+ KEY_F6 = b'\x3f'
118
+ KEY_F7 = b'\x40'
119
+ KEY_F8 = b'\x41'
120
+ KEY_F9 = b'\x42'
121
+ KEY_F10 = b'\x43'
122
+ KEY_F11 = b'\x44'
123
+ KEY_F12 = b'\x45'
124
+ KEY_PRINT_SCREEN = b'\x46'
125
+ KEY_SCROLL_LOCK = b'\x47'
126
+ KEY_PAUSE = b'\x48'
127
+ #
128
+ #
129
+ #
130
+ #
131
+ #
132
+ #
133
+ #
134
+ #
135
+ #
136
+ #
137
+ #
138
+ #
139
+ #
140
+ #
141
+ #
142
+ #
143
+ #
144
+ #
145
+ #
146
+ #
147
+ # Japanese
148
+ # Japanese
149
+ # Japanese
150
+ # KoreaKC-L
151
+ # KoreaKC-R
152
+ # ACPI
153
+ # ACPI
154
+ # ACPI
155
+ KEY_WIN_L = b'\xe3'
156
+ KEY_WIN_R = b'\xe7'
157
+ KEY_WIN_APP = b'\x65'
158
+ KEY_NONE = b'\x00'
159
+
160
+ # Reference: CH9329 chip document page 15 'CH9329 Key Code Table'
161
+ # Line number denotes the serial number in the documentation
@@ -0,0 +1,216 @@
1
+ import random
2
+ import time
3
+ from collections import OrderedDict
4
+ from typing import Dict
5
+ from typing import List
6
+ from typing import Optional
7
+ from typing import Tuple
8
+
9
+ from serial import Serial
10
+
11
+ import pych9329.frame_utils as frame_utils
12
+ from pych9329.hid_code_map import HID_CODE_MAP
13
+
14
+ MODIFIER_KEY_NAME_MAP = {
15
+ "ctrl": 0b00000001,
16
+ "ctrl_left": 0b00000001,
17
+ "shift": 0b00000010,
18
+ "shift_left": 0b00000010,
19
+ "alt": 0b00000100,
20
+ "alt_left": 0b00000100,
21
+ "win": 0b00001000,
22
+ "win_left": 0b00001000,
23
+ "ctrl_right": 0b00010000,
24
+ "shift_right": 0b00100000,
25
+ "alt_right": 0b01000000,
26
+ "win_right": 0b10000000,
27
+ }
28
+
29
+
30
+ def deduplicate_list(lt: List) -> List:
31
+ return list(OrderedDict.fromkeys(lt))
32
+
33
+
34
+ def receive_indicator_status(serial_object: Serial) -> Tuple[bool, Dict[str, bool]]:
35
+ result_dict: Dict[str, bool] = {
36
+ "usb_connect_status": False,
37
+ "num_lock": False,
38
+ "caps_lock": False,
39
+ "scroll_lock": False,
40
+ }
41
+ result_value = False
42
+ data_frame = frame_utils.DataFrame(
43
+ b"\x57\xab",
44
+ b"\x00",
45
+ b"\x01",
46
+ b"\x00",
47
+ b"",
48
+ )
49
+ frame_packet = data_frame.create_frame()
50
+ # clear connection buffer
51
+ serial_object.readall()
52
+ # write command
53
+ serial_object.write(frame_packet)
54
+ serial_object.flush()
55
+ # read packet
56
+ reply_packet: bytes = serial_object.readall()
57
+ parse_result = data_frame.parse_frame(reply_packet)
58
+ if parse_result is False:
59
+ return result_value, result_dict
60
+ reply_data = data_frame.get_data()
61
+ # 获取USB连接状态
62
+ usb_connect_status: bytes = reply_data[1:2]
63
+ if usb_connect_status == b"0x00":
64
+ result_dict["usb_connect_status"] = False
65
+ else:
66
+ result_dict["usb_connect_status"] = True
67
+ # 获取键盘锁定状态
68
+ indicator_bytes: bytes = reply_data[2:3]
69
+ indicator_flag = int.from_bytes(indicator_bytes, byteorder="little")
70
+ if indicator_flag & 0x01 != 0:
71
+ result_dict["num_lock"] = True
72
+ else:
73
+ result_dict["num_lock"] = False
74
+ if indicator_flag & 0x02 != 0:
75
+ result_dict["caps_lock"] = True
76
+ else:
77
+ result_dict["caps_lock"] = False
78
+ if indicator_flag & 0x04 != 0:
79
+ result_dict["scroll_lock"] = True
80
+ else:
81
+ result_dict["scroll_lock"] = False
82
+ result_value = True
83
+ return result_value, result_dict
84
+
85
+
86
+ def send_general_data(
87
+ serial_object: Serial,
88
+ key_tuple: Tuple[str, str, str, str, str, str] = ("", "", "", "", "", ""),
89
+ modifiers: Optional[List[str]] = None,
90
+ ) -> None:
91
+ if modifiers is None:
92
+ modifiers = []
93
+
94
+ # CMD_SEND_KB_GENERAL_DATA data has exactly 8 bytes
95
+ data = b""
96
+
97
+ # first byte modifiers key, each bit represents 1 key
98
+ #
99
+ # BIT0 - ctrl_left
100
+ # BIT1 - shift_left
101
+ # BIT2 - alt_left
102
+ # BIT3 - win_left
103
+ # BIT4 - ctrl_right
104
+ # BIT5 - shift_right
105
+ # BIT6 - alt_right
106
+ # BIT7 - win_right
107
+ modifier_value: int = 0x00
108
+ for key_name in modifiers:
109
+ if key_name not in MODIFIER_KEY_NAME_MAP:
110
+ raise InvalidModifierKey(key_name)
111
+ modifier_value |= MODIFIER_KEY_NAME_MAP[key_name]
112
+ data += modifier_value.to_bytes(1, byteorder="little")
113
+
114
+ # second byte must be 0x00
115
+ data += b"\x00"
116
+
117
+ # third to eighth bytes are keys
118
+ # we can press upto 6 buttons
119
+ for key in key_tuple:
120
+ if key not in HID_CODE_MAP:
121
+ raise InvalidKey(key)
122
+ hid_code, _ = HID_CODE_MAP[key]
123
+ data += hid_code
124
+
125
+ # create packet and send
126
+ data_frame = frame_utils.DataFrame(
127
+ b"\x57\xab",
128
+ b"\x00",
129
+ b"\x02",
130
+ b"",
131
+ b"",
132
+ )
133
+ data_frame.set_data(data)
134
+ frame_packet = data_frame.create_frame()
135
+ serial_object.write(frame_packet)
136
+ serial_object.flush()
137
+
138
+
139
+ def trigger(
140
+ serial_object: Serial,
141
+ keys: list[str],
142
+ modifiers: Optional[List[str]] = None,
143
+ ) -> None:
144
+ press_keys = keys.copy()
145
+ press_modifiers = modifiers.copy()
146
+ press_keys = deduplicate_list(press_keys)
147
+ press_modifiers = deduplicate_list(press_modifiers)
148
+ # Supports press to 6 normal buttons at the same time
149
+ if len(press_keys) > 6:
150
+ raise TooManyKeys("CH9329 supports maximum of 6 keys to be pressed at once.")
151
+ if len(modifiers) > 8:
152
+ raise TooManyKeys(
153
+ "CH9329 supports maximum of 8 control keys to be pressed at once."
154
+ )
155
+ # if len(keys) <= 6, add empty keys
156
+ while len(press_keys) != 6:
157
+ press_keys.append("")
158
+ send_general_data(
159
+ serial_object,
160
+ (
161
+ press_keys[0],
162
+ press_keys[1],
163
+ press_keys[2],
164
+ press_keys[3],
165
+ press_keys[4],
166
+ press_keys[5],
167
+ ),
168
+ press_modifiers,
169
+ )
170
+
171
+
172
+ def press(
173
+ serial_object: Serial, key: str, modifiers: Optional[List[str]] = None
174
+ ) -> None:
175
+ if modifiers is None:
176
+ modifiers_copy = []
177
+ elif isinstance(modifiers, list):
178
+ modifiers_copy = modifiers.copy()
179
+ else:
180
+ modifiers_copy = []
181
+ if key not in HID_CODE_MAP:
182
+ raise InvalidKey(key)
183
+ _, shift = HID_CODE_MAP[key]
184
+ if shift:
185
+ modifiers_copy.append("shift")
186
+ send_general_data(serial_object, (key, "", "", "", "", ""), modifiers_copy)
187
+
188
+
189
+ def release(serial_object: Serial) -> None:
190
+ send_general_data(serial_object, ("", "", "", "", "", ""))
191
+
192
+
193
+ def click(
194
+ serial_object: Serial,
195
+ key: str,
196
+ min_interval: float = 0.02,
197
+ max_interval: float = 0.05,
198
+ ) -> None:
199
+ sleep_time = random.uniform(min_interval, max_interval)
200
+ press(serial_object, key)
201
+ time.sleep(sleep_time)
202
+ release(serial_object)
203
+
204
+
205
+ def send_text_data(
206
+ ser: Serial,
207
+ text: str,
208
+ min_interval: float = 0.02,
209
+ max_interval: float = 0.05,
210
+ ) -> None:
211
+ for char in text:
212
+ click(ser, char, min_interval, max_interval)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ pass
@@ -0,0 +1,159 @@
1
+ import random
2
+ import time
3
+ import typing
4
+
5
+ from serial import Serial
6
+
7
+ import pych9329.exceptions as exceptions
8
+ import pych9329.frame_utils as frame_utils
9
+
10
+ MOUSE_BUTTON_NAME_MAP: typing.Final[dict[str, bytes]] = {
11
+ 'null': b'\x00',
12
+ 'left': b'\x01',
13
+ 'right': b'\x02',
14
+ 'center': b'\x04',
15
+ # alias
16
+ 'none': b'\x00',
17
+ 'middle': b'\x04',
18
+ }
19
+
20
+
21
+ def check_integer_range(value: int, minimum: int, maximum: int) -> bool:
22
+ if value < minimum or value > maximum:
23
+ return False
24
+ return True
25
+
26
+
27
+ def send_absolute_data(
28
+ serial_object: Serial,
29
+ x: int,
30
+ y: int,
31
+ button_name: str = 'null',
32
+ x_max: int = 1920,
33
+ y_max: int = 1080,
34
+ wheel_value: int = 0,
35
+ ) -> None:
36
+ if not check_integer_range(x_max, 0, 4096):
37
+ raise exceptions.InvalidCoordinateValue(
38
+ 'Max coordinate value should be between 0 and 4096'
39
+ )
40
+ if not check_integer_range(y_max, 0, 4096):
41
+ raise exceptions.InvalidCoordinateValue(
42
+ 'Max coordinate value should be between 0 and 4096'
43
+ )
44
+ data_frame = frame_utils.DataFrame(
45
+ b'\x57\xab',
46
+ b'\x00',
47
+ b'\x04',
48
+ b'',
49
+ b'',
50
+ )
51
+ # CMD_SEND_MS_ABS_DATA requires 7 bytes data
52
+ # first byte is always 0x02
53
+ buffer = b'\x02'
54
+
55
+ # second byte is mouse button value
56
+ buffer += MOUSE_BUTTON_NAME_MAP[button_name]
57
+
58
+ # third and fourth bytes are x-coordinates
59
+ x_value = (4096 * x) // x_max
60
+ buffer += x_value.to_bytes(2, byteorder='little')
61
+
62
+ # fifth and sixth bytes are y-coordinates
63
+ y_value = (4096 * y) // y_max
64
+ buffer += y_value.to_bytes(2, byteorder='little')
65
+
66
+ # seventh byte contains wheel data
67
+ # If it is 0x00, it means there is no scrolling action
68
+ # 0x01-0x7F, means scroll up
69
+ # 0x81-0xFF, means scroll down
70
+ if not check_integer_range(wheel_value, -128, 127):
71
+ raise exceptions.InvalidWheelValue(
72
+ 'Wheel value should be between -127 and 128'
73
+ )
74
+ buffer += wheel_value.to_bytes(1, byteorder='little', signed=True)
75
+ data_frame.set_data(buffer)
76
+ request_packet = data_frame.create_frame()
77
+ serial_object.write(request_packet)
78
+
79
+
80
+ def send_relative_data(
81
+ serial_object: Serial,
82
+ x: int,
83
+ y: int,
84
+ button_name: str = 'null',
85
+ wheel_value: int = 0,
86
+ ) -> None:
87
+ data_frame = frame_utils.DataFrame(
88
+ b'\x57\xab',
89
+ b'\x00',
90
+ b'\x05',
91
+ b'',
92
+ b'',
93
+ )
94
+ # CMD_SEND_MS_REL_DATA requires 5 bytes data
95
+ # first byte is always 0x01
96
+ buffer = b'\x01'
97
+ buffer += MOUSE_BUTTON_NAME_MAP[button_name]
98
+
99
+ # x value
100
+ if not check_integer_range(x, -128, 127):
101
+ raise exceptions.InvalidCoordinateValue(
102
+ 'Coordinate value should be between -127 and 128'
103
+ )
104
+ buffer += x.to_bytes(1, byteorder='little', signed=True)
105
+ # y value
106
+ if not check_integer_range(y, -128, 127):
107
+ raise exceptions.InvalidCoordinateValue(
108
+ 'Coordinate value should be between -127 and 128'
109
+ )
110
+ buffer += y.to_bytes(1, byteorder='little', signed=True)
111
+
112
+ # wheel value
113
+ if not check_integer_range(wheel_value, -128, 127):
114
+ raise exceptions.InvalidWheelValue(
115
+ 'Wheel value should be between -127 and 128'
116
+ )
117
+ buffer += wheel_value.to_bytes(1, byteorder='little', signed=True)
118
+
119
+ data_frame.set_data(buffer)
120
+ request_packet = data_frame.create_frame()
121
+ serial_object.write(request_packet)
122
+
123
+
124
+ def move(
125
+ serial_object: Serial,
126
+ x: int,
127
+ y: int,
128
+ relative_mode: bool = False,
129
+ monitor_width: int = 1920,
130
+ monitor_height: int = 1080,
131
+ ) -> None:
132
+ if relative_mode:
133
+ send_relative_data(serial_object, x, y, 'null')
134
+ else:
135
+ send_absolute_data(serial_object, x, y, 'null', monitor_width, monitor_height)
136
+
137
+
138
+ def press(serial_object: Serial, button_name: str = 'left') -> None:
139
+ send_relative_data(serial_object, 0, 0, button_name, 0)
140
+
141
+
142
+ def release(serial_object: Serial) -> None:
143
+ send_relative_data(serial_object, 0, 0, 'null', 0)
144
+
145
+
146
+ def click(serial_object: Serial, button_name: str = 'left') -> None:
147
+ press(serial_object, button_name)
148
+ # 100 to 400 milliseconds delay for simulating natural behavior
149
+ sleep_time = random.uniform(0.20, 0.40)
150
+ time.sleep(sleep_time)
151
+ release(serial_object)
152
+
153
+
154
+ def wheel(serial_object: Serial, wheel_value: int = 1) -> None:
155
+ send_relative_data(serial_object, 0, 0, 'null', wheel_value=wheel_value)
156
+
157
+
158
+ if __name__ == '__main__':
159
+ pass
File without changes
@@ -0,0 +1,95 @@
1
+ import struct
2
+ import typing
3
+ import pych9329.exceptions as exceptions
4
+ from typing import Any
5
+
6
+
7
+ class StructureMeta(type):
8
+ def __init__(
9
+ cls, _cls_name: str, _bases: tuple[type, ...], _cls_dict: dict[str, Any]
10
+ ):
11
+ super().__init__(cls)
12
+ fields = getattr(cls, '_fields_', [])
13
+
14
+ offset = 0
15
+ for field_format, field_name in fields:
16
+ if field_format.startswith(('<', '>', '!', '@')):
17
+ byte_order = field_format[0]
18
+ field_format = field_format[1:]
19
+ else:
20
+ byte_order = ''
21
+ field_format = byte_order + field_format
22
+ offset += struct.calcsize(field_format)
23
+ setattr(cls, '_fields_struct_size_', offset)
24
+
25
+
26
+ class Structure(metaclass=StructureMeta):
27
+ def __init__(self, data: bytes):
28
+ self.buffer = data
29
+ self.attribute = dict()
30
+ self.load()
31
+
32
+ def load(self):
33
+ fields = self._fields_
34
+ offset = 0
35
+ for field_format, field_name in fields:
36
+ field_length = struct.calcsize(field_format)
37
+ field_data = struct.unpack_from(field_format, self.buffer, offset)
38
+ if len(field_data) == 1:
39
+ self.attribute[field_name] = field_data[0]
40
+ elif len(field_data) == field_length:
41
+ buffer = bytearray()
42
+ for item in field_data:
43
+ buffer += item
44
+ self.attribute[field_name] = bytes(buffer)
45
+ else:
46
+ self.attribute[field_name] = None
47
+ offset += struct.calcsize(field_format)
48
+
49
+ def save(self):
50
+ fields = self._fields_
51
+ binary_data = bytearray()
52
+ for field_format, field_name in fields:
53
+ field_data = self.attribute[field_name]
54
+ field_bytes = struct.pack(field_format, field_data)
55
+ binary_data += bytearray(field_bytes)
56
+ self.buffer = bytes(binary_data)
57
+
58
+ def get(self, key: str) -> Any:
59
+ data = self.attribute.get(key, None)
60
+ return data
61
+
62
+ def set(self, key: str, value: Any) -> None:
63
+ self.attribute[key] = value
64
+
65
+ def buffer_load(self, buffer: bytes) -> None:
66
+ self.buffer = buffer
67
+ self.attribute.clear()
68
+ self.load()
69
+
70
+ def buffer_flush(self) -> None:
71
+ self.save()
72
+
73
+ @classmethod
74
+ def from_buffer(cls, buffer: bytes):
75
+ buffer_len = len(buffer)
76
+ if buffer_len >= cls._fields_struct_size_:
77
+ return cls(buffer)
78
+ else:
79
+ raise exceptions.BufferTooSmall()
80
+
81
+ @classmethod
82
+ def from_file(cls, buffer: typing.BinaryIO):
83
+ return cls(buffer.read(cls._fields_struct_size_))
84
+
85
+
86
+ '''
87
+ class ExampleStruct(Structure):
88
+ _fields_ = [
89
+ ('d', 'min_x'),
90
+ ('d', 'min_y'),
91
+ ]
92
+ '''
93
+
94
+ if __name__ == '__main__':
95
+ pass