makcu 0.1.4__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.
makcu/connection.py CHANGED
@@ -1,290 +1,462 @@
1
1
  import serial
2
2
  import threading
3
3
  import time
4
+ import struct
5
+ from typing import Optional, Dict, Callable, List, Any, Tuple, Union
4
6
  from serial.tools import list_ports
7
+ from dataclasses import dataclass, field
8
+ from collections import deque
9
+ from concurrent.futures import Future
10
+ import logging
11
+ import asyncio
12
+ import re
5
13
  from .errors import MakcuConnectionError, MakcuTimeoutError
6
14
  from .enums import MouseButton
7
15
 
16
+ logger = logging.getLogger(__name__)
17
+
18
+ @dataclass
19
+ class PendingCommand:
20
+ command_id: int
21
+ command: str
22
+ future: Future
23
+ timestamp: float
24
+ expect_response: bool = True
25
+ timeout: float = 0.1
26
+
27
+ @dataclass
28
+ class ParsedResponse:
29
+ command_id: Optional[int]
30
+ content: str
31
+ is_button_data: bool = False
32
+ button_mask: Optional[int] = None
33
+
8
34
  class SerialTransport:
9
- baud_change_command = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
35
+
36
+ BAUD_CHANGE_COMMAND = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
37
+ DEFAULT_TIMEOUT = 0.1
38
+ MAX_RECONNECT_ATTEMPTS = 3
39
+ RECONNECT_DELAY = 0.1
40
+
10
41
 
11
- button_map = {
12
- 0: 'left',
13
- 1: 'right',
14
- 2: 'middle',
15
- 3: 'mouse4',
16
- 4: 'mouse5'
17
- }
42
+ BUTTON_MAP = (
43
+ 'left', 'right', 'middle', 'mouse4', 'mouse5'
44
+ )
45
+
46
+ BUTTON_ENUM_MAP = (
47
+ MouseButton.LEFT,
48
+ MouseButton.RIGHT,
49
+ MouseButton.MIDDLE,
50
+ MouseButton.MOUSE4,
51
+ MouseButton.MOUSE5,
52
+ )
53
+
54
+ def __init__(self, fallback: str = "", debug: bool = False,
55
+ send_init: bool = True, auto_reconnect: bool = True,
56
+ override_port: bool = False) -> None:
18
57
 
19
- def __init__(self, fallback, debug=False, send_init=True):
20
58
  self._fallback_com_port = fallback
21
- self._log_messages = []
22
59
  self.debug = debug
23
60
  self.send_init = send_init
24
- self._button_callback = None
25
- self._last_mask = 0
26
- self._lock = threading.Lock()
61
+ self.auto_reconnect = auto_reconnect
62
+ self.override_port = override_port
63
+
64
+
27
65
  self._is_connected = False
28
- self._stop_event = threading.Event()
29
- self._listener_thread = None
30
- self._button_states = {btn: False for btn in self.button_map.values()}
31
- self._last_callback_time = {bit: 0 for bit in self.button_map}
66
+ self._reconnect_attempts = 0
67
+ self.port: Optional[str] = None
68
+ self.baudrate = 115200
69
+ self.serial: Optional[serial.Serial] = None
70
+ self._current_baud: Optional[int] = None
32
71
 
33
- self._response_buffer = ""
34
- self._response_ready = threading.Event()
35
- self._waiting_for_response = False
36
- self._response_timeout = 0.01
72
+
73
+ self._command_counter = 0
74
+ self._pending_commands: Dict[int, PendingCommand] = {}
37
75
  self._command_lock = threading.Lock()
76
+
38
77
 
39
- self._button_enum_map = {
40
- 0: MouseButton.LEFT,
41
- 1: MouseButton.RIGHT,
42
- 2: MouseButton.MIDDLE,
43
- 3: MouseButton.MOUSE4,
44
- 4: MouseButton.MOUSE5,
45
- }
78
+ self._parse_buffer = bytearray(1024)
79
+ self._buffer_pos = 0
80
+ self._response_queue = deque(maxlen=100)
81
+
46
82
 
47
- self.port = self.find_com_port()
48
- if not self.port:
49
- raise MakcuConnectionError("Makcu device not found. Please specify a port explicitly.")
83
+ self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
84
+ self._last_button_mask = 0
85
+ self._button_states = 0
86
+
50
87
 
51
- self.baudrate = 115200
52
- self.serial = None
53
- self._current_baud = None
88
+ self._stop_event = threading.Event()
89
+ self._listener_thread: Optional[threading.Thread] = None
90
+
54
91
 
55
- def receive_response(self, sent_command: str = "") -> str:
56
- try:
57
- if self._response_ready.wait(timeout=self._response_timeout):
58
- response = self._response_buffer
59
- self._response_buffer = ""
60
- self._response_ready.clear()
61
-
62
- lines = response.strip().split('\n')
63
- lines = [line.strip() for line in lines if line.strip()]
92
+ self._log_messages: deque = deque(maxlen=100)
93
+
94
+
95
+ self._ascii_decode_table = bytes(range(128))
96
+
97
+ def _log(self, message: str, level: str = "INFO") -> None:
98
+ if not self.debug and level == "DEBUG":
99
+ return
100
+
101
+ if self.debug:
102
+
103
+ timestamp = f"{time.time():.3f}"
104
+ entry = f"[{timestamp}] [{level}] {message}"
105
+ self._log_messages.append(entry)
106
+ print(entry, flush=True)
107
+
108
+ def _generate_command_id(self) -> int:
109
+ self._command_counter = (self._command_counter + 1) & 0x2710
110
+ return self._command_counter
111
+
112
+ def find_com_port(self) -> Optional[str]:
113
+ if self.override_port:
114
+ return self._fallback_com_port
115
+
116
+
117
+ target_hwid = "VID:PID=1A86:55D3"
118
+
119
+ for port in list_ports.comports():
120
+ if target_hwid in port.hwid.upper():
121
+ self._log(f"Device found: {port.device}")
122
+ return port.device
123
+
124
+ if self._fallback_com_port:
125
+ self._log(f"Using fallback: {self._fallback_com_port}")
126
+ return self._fallback_com_port
127
+
128
+ return None
129
+
130
+ def _parse_response_line(self, line: bytes) -> ParsedResponse:
131
+
132
+ if line.startswith(b'>>> '):
133
+ content = line[4:].decode('ascii', 'ignore').strip()
134
+ return ParsedResponse(None, content, False)
135
+
136
+ content = line.decode('ascii', 'ignore').strip()
137
+ return ParsedResponse(None, content, False)
138
+
139
+ def _handle_button_data(self, byte_val: int) -> None:
140
+ if byte_val == self._last_button_mask:
141
+ return
142
+
143
+ changed_bits = byte_val ^ self._last_button_mask
144
+
145
+
146
+ for bit in range(5):
147
+ if changed_bits & (1 << bit):
148
+ is_pressed = bool(byte_val & (1 << bit))
64
149
 
65
- command_clean = sent_command.strip()
66
- cleaned_lines = []
150
+
151
+ if is_pressed:
152
+ self._button_states |= (1 << bit)
153
+ else:
154
+ self._button_states &= ~(1 << bit)
67
155
 
68
- for line in lines:
69
- if not line:
70
- continue
71
- if line == command_clean:
72
- continue
73
- if line.startswith('>>> '):
74
- actual_response = line[4:].strip()
75
- if actual_response and actual_response != command_clean:
76
- cleaned_lines.append(actual_response)
77
- continue
78
- if line == command_clean:
79
- continue
80
- cleaned_lines.append(line)
156
+ if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
157
+ try:
158
+ self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
159
+ except Exception:
160
+ pass
161
+
162
+ self._last_button_mask = byte_val
163
+
164
+ def _process_pending_commands(self, content: str) -> None:
165
+ if not content or not self._pending_commands:
166
+ return
167
+
168
+ with self._command_lock:
169
+ if not self._pending_commands:
170
+ return
81
171
 
82
- result = "\n".join(cleaned_lines)
83
- if self.debug:
84
- self._log(f"Command: {command_clean} -> Response: '{result}'")
85
- return result
172
+
173
+ oldest_id = next(iter(self._pending_commands))
174
+ pending = self._pending_commands[oldest_id]
175
+
176
+ if pending.future.done():
177
+ return
178
+
179
+
180
+ if content == pending.command:
181
+ if not pending.expect_response:
182
+ pending.future.set_result(pending.command)
183
+ del self._pending_commands[oldest_id]
86
184
  else:
87
- return ""
185
+ pending.future.set_result(content)
186
+ del self._pending_commands[oldest_id]
187
+
188
+ def _cleanup_timed_out_commands(self) -> None:
189
+ if not self._pending_commands:
190
+ return
191
+
192
+ current_time = time.time()
193
+ with self._command_lock:
194
+
195
+ timed_out = [
196
+ (cmd_id, pending)
197
+ for cmd_id, pending in self._pending_commands.items()
198
+ if current_time - pending.timestamp > pending.timeout
199
+ ]
200
+
201
+
202
+ for cmd_id, pending in timed_out:
203
+ del self._pending_commands[cmd_id]
204
+ if not pending.future.done():
205
+ pending.future.set_exception(
206
+ MakcuTimeoutError(f"Command #{cmd_id} timed out")
207
+ )
208
+
209
+
210
+ def _listen(self) -> None:
211
+
212
+ read_buffer = bytearray(4096)
213
+ line_buffer = bytearray(256)
214
+ line_pos = 0
215
+
216
+
217
+ serial_read = self.serial.read
218
+ serial_in_waiting = lambda: self.serial.in_waiting
219
+ is_connected = lambda: self._is_connected
220
+ stop_requested = self._stop_event.is_set
221
+
222
+
223
+ last_cleanup = time.time()
224
+ cleanup_interval = 0.05
225
+
226
+ while is_connected() and not stop_requested():
227
+ try:
228
+
229
+ bytes_available = serial_in_waiting()
230
+ if not bytes_available:
231
+ time.sleep(0.001)
232
+ continue
88
233
 
89
- except Exception as e:
90
- if self.debug:
91
- self._log(f"Error in receive_response: {e}")
92
- return ""
93
234
 
94
- def set_button_callback(self, callback):
95
- self._button_callback = callback
235
+ bytes_read = serial_read(min(bytes_available, 4096))
236
+
96
237
 
97
- def _log(self, message):
98
- timestamp = time.strftime("%H:%M:%S")
99
- entry = f"[{timestamp}] {message}"
100
- self._log_messages.append(entry)
101
- if len(self._log_messages) > 20:
102
- self._log_messages.pop(0)
103
- print(entry, flush=True)
238
+ for byte_val in bytes_read:
104
239
 
105
- def find_com_port(self):
106
- self._log("Searching for CH343 device...")
240
+ if byte_val < 32 and byte_val not in (0x0D, 0x0A):
241
+ self._handle_button_data(byte_val)
242
+ else:
107
243
 
108
- for port in list_ports.comports():
109
- if "VID:PID=1A86:55D3" in port.hwid.upper():
110
- self._log(f"Device found: {port.device}")
111
- return port.device
244
+ if byte_val == 0x0A:
245
+ if line_pos > 0:
112
246
 
113
- if self._fallback_com_port:
114
- self._log(f"Device not found. Falling back to specified port: {self._fallback_com_port}")
115
- return self._fallback_com_port
116
- else:
117
- self._log("Fallback port not specified or invalid.")
118
- return None
247
+ line = bytes(line_buffer[:line_pos])
248
+ line_pos = 0
249
+
250
+ if line:
251
+ response = self._parse_response_line(line)
252
+ if response.content:
253
+ self._process_pending_commands(response.content)
254
+ elif byte_val != 0x0D:
255
+ if line_pos < 256:
256
+ line_buffer[line_pos] = byte_val
257
+ line_pos += 1
258
+
259
+
260
+ current_time = time.time()
261
+ if current_time - last_cleanup > cleanup_interval:
262
+ self._cleanup_timed_out_commands()
263
+ last_cleanup = current_time
264
+
265
+ except serial.SerialException:
266
+ if self.auto_reconnect:
267
+ self._attempt_reconnect()
268
+ else:
269
+ break
270
+ except Exception:
271
+ pass
119
272
 
120
- def _open_serial_port(self, port, baud_rate):
273
+ def _attempt_reconnect(self) -> None:
274
+ if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
275
+ self._is_connected = False
276
+ return
277
+
278
+ self._reconnect_attempts += 1
279
+
121
280
  try:
122
- self._log(f"Trying to open {port} at {baud_rate} baud.")
123
- return serial.Serial(port, baud_rate, timeout=0.05)
124
- except serial.SerialException:
125
- self._log(f"Failed to open {port} at {baud_rate} baud.")
126
- return None
281
+ if self.serial and self.serial.is_open:
282
+ self.serial.close()
283
+
284
+ time.sleep(self.RECONNECT_DELAY)
285
+
286
+ self.port = self.find_com_port()
287
+ if not self.port:
288
+ raise MakcuConnectionError("Device not found")
289
+
127
290
 
128
- def _change_baud_to_4M(self):
291
+ self.serial = serial.Serial(
292
+ self.port,
293
+ self.baudrate,
294
+ timeout=0.001,
295
+ write_timeout=0.01
296
+ )
297
+ self._change_baud_to_4M()
298
+
299
+ if self.send_init:
300
+ self.serial.write(b"km.buttons(1)\r")
301
+ self.serial.flush()
302
+
303
+ self._reconnect_attempts = 0
304
+
305
+ except Exception:
306
+ time.sleep(self.RECONNECT_DELAY)
307
+
308
+ def _change_baud_to_4M(self) -> bool:
129
309
  if self.serial and self.serial.is_open:
130
- self._log("Sending baud rate switch command to 4M.")
131
- self.serial.write(self.baud_change_command)
310
+ self.serial.write(self.BAUD_CHANGE_COMMAND)
132
311
  self.serial.flush()
133
- time.sleep(0.05)
312
+ time.sleep(0.02)
134
313
  self.serial.baudrate = 4000000
135
314
  self._current_baud = 4000000
136
- self._log("Switched to 4M baud successfully.")
137
315
  return True
138
316
  return False
139
317
 
140
- def connect(self):
318
+ def connect(self) -> None:
141
319
  if self._is_connected:
142
- self._log("Already connected.")
143
320
  return
144
- self.serial = self._open_serial_port(self.port, 115200)
145
- if not self.serial:
146
- raise MakcuConnectionError(f"Failed to connect to {self.port} at 115200.")
147
- self._log(f"Connected to {self.port} at 115200.")
148
- if not self._change_baud_to_4M():
149
- raise MakcuConnectionError("Failed to switch to 4M baud.")
150
- self._is_connected = True
151
- if self.send_init:
152
- with self._lock:
321
+
322
+ if not self.override_port:
323
+ self.port = self.find_com_port()
324
+ else:
325
+ self.port = self._fallback_com_port
326
+
327
+ if not self.port:
328
+ raise MakcuConnectionError("Makcu device not found")
329
+
330
+ try:
331
+
332
+ self.serial = serial.Serial(
333
+ self.port,
334
+ 115200,
335
+ timeout=0.001,
336
+ write_timeout=0.01,
337
+ xonxoff=False,
338
+ rtscts=False,
339
+ dsrdtr=False
340
+ )
341
+
342
+ if not self._change_baud_to_4M():
343
+ raise MakcuConnectionError("Failed to switch to 4M baud")
344
+
345
+ self._is_connected = True
346
+ self._reconnect_attempts = 0
347
+
348
+ if self.send_init:
153
349
  self.serial.write(b"km.buttons(1)\r")
154
350
  self.serial.flush()
155
- self._log("Sent init command: km.buttons(1)")
351
+
156
352
 
157
353
  self._stop_event.clear()
158
- self._listener_thread = threading.Thread(target=self._listen, kwargs={"debug": self.debug}, daemon=True)
354
+ self._listener_thread = threading.Thread(
355
+ target=self._listen,
356
+ daemon=True,
357
+ name="MakcuListener"
358
+ )
159
359
  self._listener_thread.start()
360
+
361
+ except Exception as e:
362
+ if self.serial:
363
+ self.serial.close()
364
+ raise MakcuConnectionError(f"Failed to connect: {e}")
160
365
 
161
- def disconnect(self):
366
+ def disconnect(self) -> None:
367
+ self._is_connected = False
368
+
162
369
  if self.send_init:
163
370
  self._stop_event.set()
164
- if self._listener_thread:
165
- self._listener_thread.join()
166
- with self._lock:
167
- if self.serial and self.serial.is_open:
168
- self.serial.close()
169
- self.serial = None
170
- self._is_connected = False
171
- self._log("Disconnected.")
172
-
173
- def is_connected(self):
174
- return self._is_connected
371
+ if self._listener_thread and self._listener_thread.is_alive():
372
+ self._listener_thread.join(timeout=0.1)
373
+
374
+ with self._command_lock:
375
+ for pending in self._pending_commands.values():
376
+ if not pending.future.done():
377
+ pending.future.cancel()
378
+ self._pending_commands.clear()
379
+
380
+ if self.serial and self.serial.is_open:
381
+ self.serial.close()
382
+ self.serial = None
175
383
 
176
- def send_command(self, command, expect_response=False):
384
+ def send_command(self, command: str, expect_response: bool = False,
385
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
177
386
  if not self._is_connected or not self.serial or not self.serial.is_open:
178
- raise MakcuConnectionError("Serial connection not open.")
387
+ raise MakcuConnectionError("Not connected")
388
+
389
+ if not expect_response:
390
+ self.serial.write(f"{command}\r\n".encode('ascii'))
391
+ self.serial.flush()
392
+ return command
393
+
394
+ cmd_id = self._generate_command_id()
395
+ tagged_command = f"{command}#{cmd_id}"
396
+
397
+ future = Future()
179
398
 
180
399
  with self._command_lock:
181
- try:
182
- if expect_response:
183
- self._response_buffer = ""
184
- self._response_ready.clear()
185
- self._waiting_for_response = True
186
-
187
- self.serial.write(command.encode("ascii") + b"\r\n")
188
- self.serial.flush()
189
-
190
- if expect_response:
191
- response = self.receive_response(sent_command=command)
192
- self._waiting_for_response = False
193
- if not response:
194
- raise MakcuTimeoutError(f"No response from device for command: {command}")
195
- return response
196
-
197
- except Exception as e:
198
- self._waiting_for_response = False
199
- raise
400
+ self._pending_commands[cmd_id] = PendingCommand(
401
+ command_id=cmd_id,
402
+ command=command,
403
+ future=future,
404
+ timestamp=time.time(),
405
+ expect_response=expect_response,
406
+ timeout=timeout
407
+ )
408
+
409
+ try:
410
+ self.serial.write(f"{tagged_command}\r\n".encode('ascii'))
411
+ self.serial.flush()
412
+
413
+ result = future.result(timeout=timeout)
414
+ return result.split('#')[0] if '#' in result else result
415
+
416
+ except TimeoutError:
417
+ raise MakcuTimeoutError(f"Command timed out: {command}")
418
+ except Exception as e:
419
+ with self._command_lock:
420
+ self._pending_commands.pop(cmd_id, None)
421
+ raise
200
422
 
201
- def get_button_states(self):
202
- return dict(self._button_states)
423
+ async def async_send_command(self, command: str, expect_response: bool = False,
424
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
425
+ loop = asyncio.get_running_loop()
426
+ return await loop.run_in_executor(
427
+ None, self.send_command, command, expect_response, timeout
428
+ )
203
429
 
204
- def get_button_mask(self) -> int:
205
- return self._last_mask
430
+ def is_connected(self) -> bool:
431
+ return self._is_connected and self.serial is not None and self.serial.is_open
206
432
 
207
- def enable_button_monitoring(self, enable: bool = True):
208
- self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
433
+ def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
434
+ self._button_callback = callback
209
435
 
210
- def catch_button(self, button: MouseButton):
211
- command = {
212
- "LEFT": "km.catch_ml(0)",
213
- "RIGHT": "km.catch_mr(0)",
214
- "MIDDLE": "km.catch_mm(0)",
215
- "MOUSE4": "km.catch_ms1(0)",
216
- "MOUSE5": "km.catch_ms2(0)",
217
- }.get(button.upper())
218
- if command:
219
- self.send_command(command)
220
- else:
221
- raise ValueError(f"Unsupported button: {button}")
436
+ def get_button_states(self) -> Dict[str, bool]:
437
+ return {
438
+ self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
439
+ for i in range(5)
440
+ }
441
+
442
+ def get_button_mask(self) -> int:
443
+ return self._last_button_mask
222
444
 
223
- def _is_button_data(self, byte_value):
224
- return byte_value <= 0b11111 and byte_value not in [0x0D, 0x0A]
445
+ def enable_button_monitoring(self, enable: bool = True) -> None:
446
+ self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
225
447
 
226
- def _is_ascii_data(self, byte_value):
227
- return 32 <= byte_value <= 126 or byte_value in [0x0D, 0x0A] # Include CR/LF
448
+ async def __aenter__(self):
449
+ loop = asyncio.get_running_loop()
450
+ await loop.run_in_executor(None, self.connect)
451
+ return self
228
452
 
229
- def _listen(self, debug=False):
230
- self._log("Started listener thread")
231
- self._last_mask = 0
232
- ascii_buffer = bytearray()
233
- response_lines = []
453
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
454
+ loop = asyncio.get_running_loop()
455
+ await loop.run_in_executor(None, self.disconnect)
234
456
 
235
- while self._is_connected and not self._stop_event.is_set():
236
- try:
237
- data = self.serial.read(self.serial.in_waiting or 1)
238
- if not data:
239
- continue
457
+ def __enter__(self):
458
+ self.connect()
459
+ return self
240
460
 
241
- for byte_val in data:
242
- if (self._is_button_data(byte_val) and
243
- not self._waiting_for_response):
244
- if byte_val != self._last_mask:
245
- changed_bits = byte_val ^ self._last_mask
246
- for bit, name in self.button_map.items():
247
- if changed_bits & (1 << bit):
248
- is_pressed = bool(byte_val & (1 << bit))
249
- self._button_states[name] = is_pressed
250
- button_enum = self._button_enum_map.get(bit)
251
- if button_enum and self._button_callback:
252
- self._button_callback(button_enum, is_pressed)
253
-
254
- self._last_mask = byte_val
255
-
256
- if debug:
257
- pressed = [name for _, name in self.button_map.items() if self._button_states[name]]
258
- button_str = ", ".join(pressed) if pressed else "No buttons pressed"
259
- self._log(f"Mask: 0x{byte_val:02X} -> {button_str}")
260
- elif self._is_ascii_data(byte_val):
261
- if self._waiting_for_response:
262
- ascii_buffer.append(byte_val)
263
- if byte_val == 0x0A:
264
- try:
265
- line = ascii_buffer.decode('ascii', errors='ignore').strip()
266
- ascii_buffer.clear()
267
-
268
- if line:
269
- response_lines.append(line)
270
-
271
- if (len(response_lines) >= 2 or
272
- (len(response_lines) == 1 and not line.startswith('>>> '))):
273
-
274
- full_response = '\n'.join(response_lines)
275
- self._response_buffer = full_response
276
- self._response_ready.set()
277
- response_lines.clear()
278
-
279
- except Exception as e:
280
- self._log(f"Error decoding ASCII response: {e}")
281
- ascii_buffer.clear()
282
- response_lines.clear()
283
-
284
- except serial.SerialException as e:
285
- if "ClearCommError failed" not in str(e):
286
- self._log(f"Serial error during listening: {e}")
287
- break
288
- except Exception as e:
289
- self._log(f"Unexpected error in listener: {e}")
290
- self._log("Listener thread exiting")
461
+ def __exit__(self, exc_type, exc_val, exc_tb):
462
+ self.disconnect()