autoglm-gui 1.5.0__py3-none-any.whl → 1.5.2__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.
Files changed (97) hide show
  1. AutoGLM_GUI/__init__.py +1 -1
  2. AutoGLM_GUI/__main__.py +11 -2
  3. AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
  4. AutoGLM_GUI/agents/__init__.py +7 -2
  5. AutoGLM_GUI/agents/factory.py +46 -6
  6. AutoGLM_GUI/agents/glm/agent.py +8 -3
  7. AutoGLM_GUI/agents/glm/async_agent.py +515 -0
  8. AutoGLM_GUI/agents/glm/parser.py +4 -2
  9. AutoGLM_GUI/agents/mai/agent.py +3 -0
  10. AutoGLM_GUI/agents/protocols.py +111 -1
  11. AutoGLM_GUI/agents/stream_runner.py +11 -7
  12. AutoGLM_GUI/api/__init__.py +3 -1
  13. AutoGLM_GUI/api/agents.py +103 -37
  14. AutoGLM_GUI/api/devices.py +72 -0
  15. AutoGLM_GUI/api/history.py +27 -1
  16. AutoGLM_GUI/api/layered_agent.py +9 -8
  17. AutoGLM_GUI/api/mcp.py +6 -4
  18. AutoGLM_GUI/config_manager.py +38 -1
  19. AutoGLM_GUI/device_manager.py +28 -4
  20. AutoGLM_GUI/device_metadata_manager.py +174 -0
  21. AutoGLM_GUI/devices/mock_device.py +8 -1
  22. AutoGLM_GUI/models/history.py +45 -1
  23. AutoGLM_GUI/phone_agent_manager.py +145 -32
  24. AutoGLM_GUI/scheduler_manager.py +52 -6
  25. AutoGLM_GUI/schemas.py +101 -0
  26. AutoGLM_GUI/scrcpy_stream.py +2 -1
  27. AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
  28. AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
  29. AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
  30. AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
  31. AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
  32. AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
  33. AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
  34. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  35. AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
  36. AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
  37. AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
  38. AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
  39. AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
  40. AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
  41. AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
  42. AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
  43. AutoGLM_GUI/static/index.html +2 -2
  44. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
  45. autoglm_gui-1.5.2.dist-info/RECORD +119 -0
  46. AutoGLM_GUI/device_adapter.py +0 -263
  47. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
  48. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
  49. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
  50. AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
  51. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
  52. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
  53. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
  54. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
  55. autoglm_gui-1.5.0.dist-info/RECORD +0 -157
  56. mai_agent/base.py +0 -137
  57. mai_agent/mai_grounding_agent.py +0 -263
  58. mai_agent/mai_naivigation_agent.py +0 -526
  59. mai_agent/prompt.py +0 -148
  60. mai_agent/unified_memory.py +0 -67
  61. mai_agent/utils.py +0 -73
  62. phone_agent/__init__.py +0 -12
  63. phone_agent/actions/__init__.py +0 -5
  64. phone_agent/actions/handler.py +0 -400
  65. phone_agent/actions/handler_ios.py +0 -278
  66. phone_agent/adb/__init__.py +0 -51
  67. phone_agent/adb/connection.py +0 -358
  68. phone_agent/adb/device.py +0 -253
  69. phone_agent/adb/input.py +0 -108
  70. phone_agent/adb/screenshot.py +0 -108
  71. phone_agent/agent.py +0 -253
  72. phone_agent/agent_ios.py +0 -277
  73. phone_agent/config/__init__.py +0 -53
  74. phone_agent/config/apps.py +0 -227
  75. phone_agent/config/apps_harmonyos.py +0 -256
  76. phone_agent/config/apps_ios.py +0 -339
  77. phone_agent/config/i18n.py +0 -81
  78. phone_agent/config/prompts.py +0 -80
  79. phone_agent/config/prompts_en.py +0 -79
  80. phone_agent/config/prompts_zh.py +0 -82
  81. phone_agent/config/timing.py +0 -167
  82. phone_agent/device_factory.py +0 -166
  83. phone_agent/hdc/__init__.py +0 -53
  84. phone_agent/hdc/connection.py +0 -384
  85. phone_agent/hdc/device.py +0 -269
  86. phone_agent/hdc/input.py +0 -145
  87. phone_agent/hdc/screenshot.py +0 -127
  88. phone_agent/model/__init__.py +0 -5
  89. phone_agent/model/client.py +0 -290
  90. phone_agent/xctest/__init__.py +0 -47
  91. phone_agent/xctest/connection.py +0 -379
  92. phone_agent/xctest/device.py +0 -472
  93. phone_agent/xctest/input.py +0 -311
  94. phone_agent/xctest/screenshot.py +0 -226
  95. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
  96. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
  97. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,278 +0,0 @@
1
- """Action handler for iOS automation using WebDriverAgent."""
2
-
3
- import time
4
- from dataclasses import dataclass
5
- from typing import Any, Callable
6
-
7
- from phone_agent.xctest import (
8
- back,
9
- double_tap,
10
- home,
11
- launch_app,
12
- long_press,
13
- swipe,
14
- tap,
15
- )
16
- from phone_agent.xctest.input import clear_text, hide_keyboard, type_text
17
-
18
-
19
- @dataclass
20
- class ActionResult:
21
- """Result of an action execution."""
22
-
23
- success: bool
24
- should_finish: bool
25
- message: str | None = None
26
- requires_confirmation: bool = False
27
-
28
-
29
- class IOSActionHandler:
30
- """
31
- Handles execution of actions from AI model output for iOS devices.
32
-
33
- Args:
34
- wda_url: WebDriverAgent URL.
35
- session_id: Optional WDA session ID.
36
- confirmation_callback: Optional callback for sensitive action confirmation.
37
- Should return True to proceed, False to cancel.
38
- takeover_callback: Optional callback for takeover requests (login, captcha).
39
- """
40
-
41
- def __init__(
42
- self,
43
- wda_url: str = "http://localhost:8100",
44
- session_id: str | None = None,
45
- confirmation_callback: Callable[[str], bool] | None = None,
46
- takeover_callback: Callable[[str], None] | None = None,
47
- ):
48
- self.wda_url = wda_url
49
- self.session_id = session_id
50
- self.confirmation_callback = confirmation_callback or self._default_confirmation
51
- self.takeover_callback = takeover_callback or self._default_takeover
52
-
53
- def execute(
54
- self, action: dict[str, Any], screen_width: int, screen_height: int
55
- ) -> ActionResult:
56
- """
57
- Execute an action from the AI model.
58
-
59
- Args:
60
- action: The action dictionary from the model.
61
- screen_width: Current screen width in pixels.
62
- screen_height: Current screen height in pixels.
63
-
64
- Returns:
65
- ActionResult indicating success and whether to finish.
66
- """
67
- action_type = action.get("_metadata")
68
-
69
- if action_type == "finish":
70
- return ActionResult(
71
- success=True, should_finish=True, message=action.get("message")
72
- )
73
-
74
- if action_type != "do":
75
- return ActionResult(
76
- success=False,
77
- should_finish=True,
78
- message=f"Unknown action type: {action_type}",
79
- )
80
-
81
- action_name = action.get("action")
82
- handler_method = self._get_handler(action_name)
83
-
84
- if handler_method is None:
85
- return ActionResult(
86
- success=False,
87
- should_finish=False,
88
- message=f"Unknown action: {action_name}",
89
- )
90
-
91
- try:
92
- return handler_method(action, screen_width, screen_height)
93
- except Exception as e:
94
- return ActionResult(
95
- success=False, should_finish=False, message=f"Action failed: {e}"
96
- )
97
-
98
- def _get_handler(self, action_name: str) -> Callable | None:
99
- """Get the handler method for an action."""
100
- handlers = {
101
- "Launch": self._handle_launch,
102
- "Tap": self._handle_tap,
103
- "Type": self._handle_type,
104
- "Type_Name": self._handle_type,
105
- "Swipe": self._handle_swipe,
106
- "Back": self._handle_back,
107
- "Home": self._handle_home,
108
- "Double Tap": self._handle_double_tap,
109
- "Long Press": self._handle_long_press,
110
- "Wait": self._handle_wait,
111
- "Take_over": self._handle_takeover,
112
- "Note": self._handle_note,
113
- "Call_API": self._handle_call_api,
114
- "Interact": self._handle_interact,
115
- }
116
- return handlers.get(action_name)
117
-
118
- def _convert_relative_to_absolute(
119
- self, element: list[int], screen_width: int, screen_height: int
120
- ) -> tuple[int, int]:
121
- """Convert relative coordinates (0-1000) to absolute pixels."""
122
- x = int(element[0] / 1000 * screen_width)
123
- y = int(element[1] / 1000 * screen_height)
124
- return x, y
125
-
126
- def _handle_launch(self, action: dict, width: int, height: int) -> ActionResult:
127
- """Handle app launch action."""
128
- app_name = action.get("app")
129
- if not app_name:
130
- return ActionResult(False, False, "No app name specified")
131
-
132
- success = launch_app(app_name, wda_url=self.wda_url, session_id=self.session_id)
133
- if success:
134
- return ActionResult(True, False)
135
- return ActionResult(False, False, f"App not found: {app_name}")
136
-
137
- def _handle_tap(self, action: dict, width: int, height: int) -> ActionResult:
138
- """Handle tap action."""
139
- element = action.get("element")
140
- if not element:
141
- return ActionResult(False, False, "No element coordinates")
142
-
143
- x, y = self._convert_relative_to_absolute(element, width, height)
144
-
145
- print(f"Physically tap on ({x}, {y})")
146
-
147
- # Check for sensitive operation
148
- if "message" in action:
149
- if not self.confirmation_callback(action["message"]):
150
- return ActionResult(
151
- success=False,
152
- should_finish=True,
153
- message="User cancelled sensitive operation",
154
- )
155
-
156
- tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
157
- return ActionResult(True, False)
158
-
159
- def _handle_type(self, action: dict, width: int, height: int) -> ActionResult:
160
- """Handle text input action."""
161
- text = action.get("text", "")
162
-
163
- # Clear existing text and type new text
164
- clear_text(wda_url=self.wda_url, session_id=self.session_id)
165
- time.sleep(0.5)
166
-
167
- type_text(text, wda_url=self.wda_url, session_id=self.session_id)
168
- time.sleep(0.5)
169
-
170
- # Hide keyboard after typing
171
- hide_keyboard(wda_url=self.wda_url, session_id=self.session_id)
172
- time.sleep(0.5)
173
-
174
- return ActionResult(True, False)
175
-
176
- def _handle_swipe(self, action: dict, width: int, height: int) -> ActionResult:
177
- """Handle swipe action."""
178
- start = action.get("start")
179
- end = action.get("end")
180
-
181
- if not start or not end:
182
- return ActionResult(False, False, "Missing swipe coordinates")
183
-
184
- start_x, start_y = self._convert_relative_to_absolute(start, width, height)
185
- end_x, end_y = self._convert_relative_to_absolute(end, width, height)
186
-
187
- print(f"Physically scroll from ({start_x}, {start_y}) to ({end_x}, {end_y})")
188
-
189
- swipe(
190
- start_x,
191
- start_y,
192
- end_x,
193
- end_y,
194
- wda_url=self.wda_url,
195
- session_id=self.session_id,
196
- )
197
- return ActionResult(True, False)
198
-
199
- def _handle_back(self, action: dict, width: int, height: int) -> ActionResult:
200
- """Handle back gesture (swipe from left edge)."""
201
- back(wda_url=self.wda_url, session_id=self.session_id)
202
- return ActionResult(True, False)
203
-
204
- def _handle_home(self, action: dict, width: int, height: int) -> ActionResult:
205
- """Handle home button action."""
206
- home(wda_url=self.wda_url, session_id=self.session_id)
207
- return ActionResult(True, False)
208
-
209
- def _handle_double_tap(self, action: dict, width: int, height: int) -> ActionResult:
210
- """Handle double tap action."""
211
- element = action.get("element")
212
- if not element:
213
- return ActionResult(False, False, "No element coordinates")
214
-
215
- x, y = self._convert_relative_to_absolute(element, width, height)
216
- double_tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
217
- return ActionResult(True, False)
218
-
219
- def _handle_long_press(self, action: dict, width: int, height: int) -> ActionResult:
220
- """Handle long press action."""
221
- element = action.get("element")
222
- if not element:
223
- return ActionResult(False, False, "No element coordinates")
224
-
225
- x, y = self._convert_relative_to_absolute(element, width, height)
226
- long_press(
227
- x,
228
- y,
229
- duration=3.0,
230
- wda_url=self.wda_url,
231
- session_id=self.session_id,
232
- )
233
- return ActionResult(True, False)
234
-
235
- def _handle_wait(self, action: dict, width: int, height: int) -> ActionResult:
236
- """Handle wait action."""
237
- duration_str = action.get("duration", "1 seconds")
238
- try:
239
- duration = float(duration_str.replace("seconds", "").strip())
240
- except ValueError:
241
- duration = 1.0
242
-
243
- time.sleep(duration)
244
- return ActionResult(True, False)
245
-
246
- def _handle_takeover(self, action: dict, width: int, height: int) -> ActionResult:
247
- """Handle takeover request (login, captcha, etc.)."""
248
- message = action.get("message", "User intervention required")
249
- self.takeover_callback(message)
250
- return ActionResult(True, False)
251
-
252
- def _handle_note(self, action: dict, width: int, height: int) -> ActionResult:
253
- """Handle note action (placeholder for content recording)."""
254
- # This action is typically used for recording page content
255
- # Implementation depends on specific requirements
256
- return ActionResult(True, False)
257
-
258
- def _handle_call_api(self, action: dict, width: int, height: int) -> ActionResult:
259
- """Handle API call action (placeholder for summarization)."""
260
- # This action is typically used for content summarization
261
- # Implementation depends on specific requirements
262
- return ActionResult(True, False)
263
-
264
- def _handle_interact(self, action: dict, width: int, height: int) -> ActionResult:
265
- """Handle interaction request (user choice needed)."""
266
- # This action signals that user input is needed
267
- return ActionResult(True, False, message="User interaction required")
268
-
269
- @staticmethod
270
- def _default_confirmation(message: str) -> bool:
271
- """Default confirmation callback using console input."""
272
- response = input(f"Sensitive operation: {message}\nConfirm? (Y/N): ")
273
- return response.upper() == "Y"
274
-
275
- @staticmethod
276
- def _default_takeover(message: str) -> None:
277
- """Default takeover callback using console input."""
278
- input(f"{message}\nPress Enter after completing manual operation...")
@@ -1,51 +0,0 @@
1
- """ADB utilities for Android device interaction."""
2
-
3
- from phone_agent.adb.connection import (
4
- ADBConnection,
5
- ConnectionType,
6
- DeviceInfo,
7
- list_devices,
8
- quick_connect,
9
- )
10
- from phone_agent.adb.device import (
11
- back,
12
- double_tap,
13
- get_current_app,
14
- home,
15
- launch_app,
16
- long_press,
17
- swipe,
18
- tap,
19
- )
20
- from phone_agent.adb.input import (
21
- clear_text,
22
- detect_and_set_adb_keyboard,
23
- restore_keyboard,
24
- type_text,
25
- )
26
- from phone_agent.adb.screenshot import get_screenshot
27
-
28
- __all__ = [
29
- # Screenshot
30
- "get_screenshot",
31
- # Input
32
- "type_text",
33
- "clear_text",
34
- "detect_and_set_adb_keyboard",
35
- "restore_keyboard",
36
- # Device control
37
- "get_current_app",
38
- "tap",
39
- "swipe",
40
- "back",
41
- "home",
42
- "double_tap",
43
- "long_press",
44
- "launch_app",
45
- # Connection management
46
- "ADBConnection",
47
- "DeviceInfo",
48
- "ConnectionType",
49
- "quick_connect",
50
- "list_devices",
51
- ]
@@ -1,358 +0,0 @@
1
- """ADB connection management for local and remote devices."""
2
-
3
- import subprocess
4
- import time
5
- from dataclasses import dataclass
6
- from enum import Enum
7
-
8
- from phone_agent.config.timing import TIMING_CONFIG
9
-
10
-
11
- class ConnectionType(Enum):
12
- """Type of ADB connection."""
13
-
14
- USB = "usb"
15
- WIFI = "wifi"
16
- REMOTE = "remote"
17
-
18
-
19
- @dataclass
20
- class DeviceInfo:
21
- """Information about a connected device."""
22
-
23
- device_id: str
24
- status: str
25
- connection_type: ConnectionType
26
- model: str | None = None
27
- android_version: str | None = None
28
-
29
-
30
- class ADBConnection:
31
- """
32
- Manages ADB connections to Android devices.
33
-
34
- Supports USB, WiFi, and remote TCP/IP connections.
35
-
36
- Example:
37
- >>> conn = ADBConnection()
38
- >>> # Connect to remote device
39
- >>> conn.connect("192.168.1.100:5555")
40
- >>> # List devices
41
- >>> devices = conn.list_devices()
42
- >>> # Disconnect
43
- >>> conn.disconnect("192.168.1.100:5555")
44
- """
45
-
46
- def __init__(self, adb_path: str = "adb"):
47
- """
48
- Initialize ADB connection manager.
49
-
50
- Args:
51
- adb_path: Path to ADB executable.
52
- """
53
- self.adb_path = adb_path
54
-
55
- def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
56
- """
57
- Connect to a remote device via TCP/IP.
58
-
59
- Args:
60
- address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
61
- timeout: Connection timeout in seconds.
62
-
63
- Returns:
64
- Tuple of (success, message).
65
-
66
- Note:
67
- The remote device must have TCP/IP debugging enabled.
68
- On the device, run: adb tcpip 5555
69
- """
70
- # Validate address format
71
- if ":" not in address:
72
- address = f"{address}:5555" # Default ADB port
73
-
74
- try:
75
- result = subprocess.run(
76
- [self.adb_path, "connect", address],
77
- capture_output=True,
78
- text=True,
79
- timeout=timeout,
80
- )
81
-
82
- output = result.stdout + result.stderr
83
-
84
- if "connected" in output.lower():
85
- return True, f"Connected to {address}"
86
- elif "already connected" in output.lower():
87
- return True, f"Already connected to {address}"
88
- else:
89
- return False, output.strip()
90
-
91
- except subprocess.TimeoutExpired:
92
- return False, f"Connection timeout after {timeout}s"
93
- except Exception as e:
94
- return False, f"Connection error: {e}"
95
-
96
- def disconnect(self, address: str | None = None) -> tuple[bool, str]:
97
- """
98
- Disconnect from a remote device.
99
-
100
- Args:
101
- address: Device address to disconnect. If None, disconnects all.
102
-
103
- Returns:
104
- Tuple of (success, message).
105
- """
106
- try:
107
- cmd = [self.adb_path, "disconnect"]
108
- if address:
109
- cmd.append(address)
110
-
111
- result = subprocess.run(
112
- cmd, capture_output=True, text=True, encoding="utf-8", timeout=5
113
- )
114
-
115
- output = result.stdout + result.stderr
116
- return True, output.strip() or "Disconnected"
117
-
118
- except Exception as e:
119
- return False, f"Disconnect error: {e}"
120
-
121
- def list_devices(self) -> list[DeviceInfo]:
122
- """
123
- List all connected devices.
124
-
125
- Returns:
126
- List of DeviceInfo objects.
127
- """
128
- try:
129
- result = subprocess.run(
130
- [self.adb_path, "devices", "-l"],
131
- capture_output=True,
132
- text=True,
133
- timeout=5,
134
- )
135
-
136
- devices = []
137
- for line in result.stdout.strip().split("\n")[1:]: # Skip header
138
- if not line.strip():
139
- continue
140
-
141
- parts = line.split()
142
- if len(parts) >= 2:
143
- device_id = parts[0]
144
- status = parts[1]
145
-
146
- # Determine connection type
147
- if ":" in device_id:
148
- conn_type = ConnectionType.REMOTE
149
- elif "emulator" in device_id:
150
- conn_type = ConnectionType.USB # Emulator via USB
151
- else:
152
- conn_type = ConnectionType.USB
153
-
154
- # Parse additional info
155
- model = None
156
- for part in parts[2:]:
157
- if part.startswith("model:"):
158
- model = part.split(":", 1)[1]
159
- break
160
-
161
- devices.append(
162
- DeviceInfo(
163
- device_id=device_id,
164
- status=status,
165
- connection_type=conn_type,
166
- model=model,
167
- )
168
- )
169
-
170
- return devices
171
-
172
- except Exception as e:
173
- print(f"Error listing devices: {e}")
174
- return []
175
-
176
- def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
177
- """
178
- Get detailed information about a device.
179
-
180
- Args:
181
- device_id: Device ID. If None, uses first available device.
182
-
183
- Returns:
184
- DeviceInfo or None if not found.
185
- """
186
- devices = self.list_devices()
187
-
188
- if not devices:
189
- return None
190
-
191
- if device_id is None:
192
- return devices[0]
193
-
194
- for device in devices:
195
- if device.device_id == device_id:
196
- return device
197
-
198
- return None
199
-
200
- def is_connected(self, device_id: str | None = None) -> bool:
201
- """
202
- Check if a device is connected.
203
-
204
- Args:
205
- device_id: Device ID to check. If None, checks if any device is connected.
206
-
207
- Returns:
208
- True if connected, False otherwise.
209
- """
210
- devices = self.list_devices()
211
-
212
- if not devices:
213
- return False
214
-
215
- if device_id is None:
216
- return any(d.status == "device" for d in devices)
217
-
218
- return any(d.device_id == device_id and d.status == "device" for d in devices)
219
-
220
- def enable_tcpip(
221
- self, port: int = 5555, device_id: str | None = None
222
- ) -> tuple[bool, str]:
223
- """
224
- Enable TCP/IP debugging on a USB-connected device.
225
-
226
- This allows subsequent wireless connections to the device.
227
-
228
- Args:
229
- port: TCP port for ADB (default: 5555).
230
- device_id: Device ID. If None, uses first available device.
231
-
232
- Returns:
233
- Tuple of (success, message).
234
-
235
- Note:
236
- The device must be connected via USB first.
237
- After this, you can disconnect USB and connect via WiFi.
238
- """
239
- try:
240
- cmd = [self.adb_path]
241
- if device_id:
242
- cmd.extend(["-s", device_id])
243
- cmd.extend(["tcpip", str(port)])
244
-
245
- result = subprocess.run(
246
- cmd, capture_output=True, text=True, encoding="utf-8", timeout=10
247
- )
248
-
249
- output = result.stdout + result.stderr
250
-
251
- if "restarting" in output.lower() or result.returncode == 0:
252
- time.sleep(TIMING_CONFIG.connection.adb_restart_delay)
253
- return True, f"TCP/IP mode enabled on port {port}"
254
- else:
255
- return False, output.strip()
256
-
257
- except Exception as e:
258
- return False, f"Error enabling TCP/IP: {e}"
259
-
260
- def get_device_ip(self, device_id: str | None = None) -> str | None:
261
- """
262
- Get the IP address of a connected device.
263
-
264
- Args:
265
- device_id: Device ID. If None, uses first available device.
266
-
267
- Returns:
268
- IP address string or None if not found.
269
- """
270
- try:
271
- cmd = [self.adb_path]
272
- if device_id:
273
- cmd.extend(["-s", device_id])
274
- cmd.extend(["shell", "ip", "route"])
275
-
276
- result = subprocess.run(
277
- cmd, capture_output=True, text=True, encoding="utf-8", timeout=5
278
- )
279
-
280
- # Parse IP from route output
281
- for line in result.stdout.split("\n"):
282
- if "src" in line:
283
- parts = line.split()
284
- for i, part in enumerate(parts):
285
- if part == "src" and i + 1 < len(parts):
286
- return parts[i + 1]
287
-
288
- # Alternative: try wlan0 interface
289
- cmd[-1] = "ip addr show wlan0"
290
- result = subprocess.run(
291
- cmd[:-1] + ["shell", "ip", "addr", "show", "wlan0"],
292
- capture_output=True,
293
- text=True,
294
- encoding="utf-8",
295
- timeout=5,
296
- )
297
-
298
- for line in result.stdout.split("\n"):
299
- if "inet " in line:
300
- parts = line.strip().split()
301
- if len(parts) >= 2:
302
- return parts[1].split("/")[0]
303
-
304
- return None
305
-
306
- except Exception as e:
307
- print(f"Error getting device IP: {e}")
308
- return None
309
-
310
- def restart_server(self) -> tuple[bool, str]:
311
- """
312
- Restart the ADB server.
313
-
314
- Returns:
315
- Tuple of (success, message).
316
- """
317
- try:
318
- # Kill server
319
- subprocess.run(
320
- [self.adb_path, "kill-server"], capture_output=True, timeout=5
321
- )
322
-
323
- time.sleep(TIMING_CONFIG.connection.server_restart_delay)
324
-
325
- # Start server
326
- subprocess.run(
327
- [self.adb_path, "start-server"], capture_output=True, timeout=5
328
- )
329
-
330
- return True, "ADB server restarted"
331
-
332
- except Exception as e:
333
- return False, f"Error restarting server: {e}"
334
-
335
-
336
- def quick_connect(address: str) -> tuple[bool, str]:
337
- """
338
- Quick helper to connect to a remote device.
339
-
340
- Args:
341
- address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
342
-
343
- Returns:
344
- Tuple of (success, message).
345
- """
346
- conn = ADBConnection()
347
- return conn.connect(address)
348
-
349
-
350
- def list_devices() -> list[DeviceInfo]:
351
- """
352
- Quick helper to list connected devices.
353
-
354
- Returns:
355
- List of DeviceInfo objects.
356
- """
357
- conn = ADBConnection()
358
- return conn.list_devices()