droidrun 0.3.2__py3-none-any.whl → 0.3.4__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.
@@ -18,10 +18,24 @@ logger = logging.getLogger("droidrun")
18
18
 
19
19
  class Trajectory:
20
20
 
21
- def __init__(self):
22
- """Initializes an empty trajectory class."""
21
+ def __init__(self, goal: str = None):
22
+ """Initializes an empty trajectory class.
23
+
24
+ Args:
25
+ goal: The goal/prompt that this trajectory is trying to achieve
26
+ """
23
27
  self.events: List[Event] = []
24
28
  self.screenshots: List[bytes] = []
29
+ self.macro: List[Event] = []
30
+ self.goal = goal or "DroidRun automation sequence"
31
+
32
+ def set_goal(self, goal: str) -> None:
33
+ """Update the goal/description for this trajectory.
34
+
35
+ Args:
36
+ goal: The new goal/prompt description
37
+ """
38
+ self.goal = goal
25
39
 
26
40
 
27
41
  def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
@@ -33,9 +47,10 @@ class Trajectory:
33
47
  duration: Duration for each frame in milliseconds
34
48
 
35
49
  Returns:
36
- Path to the created GIF file
50
+ Path to the created GIF file, or None if no screenshots available
37
51
  """
38
52
  if len(self.screenshots) == 0:
53
+ logger.info("📷 No screenshots available for GIF creation")
39
54
  return None
40
55
 
41
56
  images = []
@@ -62,17 +77,20 @@ class Trajectory:
62
77
  ) -> str:
63
78
  """
64
79
  Save trajectory steps to a JSON file and create a GIF of screenshots if available.
80
+ Also saves the macro sequence as a separate file for replay.
81
+ Creates a dedicated folder for each trajectory containing all related files.
65
82
 
66
83
  Args:
67
- directory: Directory to save the trajectory files
84
+ directory: Base directory to save the trajectory files
68
85
 
69
86
  Returns:
70
- Path to the saved trajectory file
87
+ Path to the trajectory folder
71
88
  """
72
89
  os.makedirs(directory, exist_ok=True)
73
90
 
74
91
  timestamp = time.strftime("%Y%m%d_%H%M%S")
75
- base_path = os.path.join(directory, f"trajectory_{timestamp}")
92
+ trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
93
+ os.makedirs(trajectory_folder, exist_ok=True)
76
94
 
77
95
  def make_serializable(obj):
78
96
  """Recursively make objects JSON serializable."""
@@ -100,6 +118,7 @@ class Trajectory:
100
118
  else:
101
119
  return obj
102
120
 
121
+ # Save main trajectory events
103
122
  serializable_events = []
104
123
  for event in self.events:
105
124
  event_dict = {
@@ -109,13 +128,209 @@ class Trajectory:
109
128
  }
110
129
  serializable_events.append(event_dict)
111
130
 
112
- json_path = f"{base_path}.json"
113
- with open(json_path, "w") as f:
131
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
132
+ with open(trajectory_json_path, "w") as f:
114
133
  json.dump(serializable_events, f, indent=2)
115
134
 
116
- self.create_screenshot_gif(base_path)
135
+ # Save macro sequence as a separate file for replay
136
+ if self.macro:
137
+ macro_data = []
138
+ for macro_event in self.macro:
139
+ macro_dict = {
140
+ "type": macro_event.__class__.__name__,
141
+ **{k: make_serializable(v) for k, v in macro_event.__dict__.items()
142
+ if not k.startswith('_')}
143
+ }
144
+ macro_data.append(macro_dict)
145
+
146
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
147
+ with open(macro_json_path, "w") as f:
148
+ json.dump({
149
+ "version": "1.0",
150
+ "description": self.goal,
151
+ "timestamp": timestamp,
152
+ "total_actions": len(macro_data),
153
+ "actions": macro_data
154
+ }, f, indent=2)
155
+
156
+ logger.info(f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}")
157
+
158
+ # Create screenshot GIF
159
+ gif_path = self.create_screenshot_gif(os.path.join(trajectory_folder, "screenshots"))
160
+ if gif_path:
161
+ logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
162
+
163
+ logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
164
+ return trajectory_folder
165
+
166
+ @staticmethod
167
+ def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
168
+ """
169
+ Load trajectory data from a trajectory folder.
170
+
171
+ Args:
172
+ trajectory_folder: Path to the trajectory folder
173
+
174
+ Returns:
175
+ Dictionary containing trajectory data, macro data, and file paths
176
+ """
177
+ result = {
178
+ "trajectory_data": None,
179
+ "macro_data": None,
180
+ "gif_path": None,
181
+ "folder_path": trajectory_folder
182
+ }
183
+
184
+ try:
185
+ # Load main trajectory
186
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
187
+ if os.path.exists(trajectory_json_path):
188
+ with open(trajectory_json_path, "r") as f:
189
+ result["trajectory_data"] = json.load(f)
190
+ logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
191
+
192
+ # Load macro sequence
193
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
194
+ if os.path.exists(macro_json_path):
195
+ with open(macro_json_path, "r") as f:
196
+ result["macro_data"] = json.load(f)
197
+ logger.info(f"📖 Loaded macro data from {macro_json_path}")
198
+
199
+ # Check for GIF
200
+ gif_path = os.path.join(trajectory_folder, "screenshots.gif")
201
+ if os.path.exists(gif_path):
202
+ result["gif_path"] = gif_path
203
+ logger.info(f"🎬 Found screenshot GIF at {gif_path}")
204
+
205
+ return result
206
+
207
+ except Exception as e:
208
+ logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
209
+ return result
210
+
211
+ @staticmethod
212
+ def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
213
+ """
214
+ Load a macro sequence from a saved macro file.
215
+
216
+ Args:
217
+ macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
218
+
219
+ Returns:
220
+ Dictionary containing the macro sequence data
221
+ """
222
+ # Check if it's a folder path - if so, look for macro.json inside
223
+ if os.path.isdir(macro_file_path):
224
+ macro_file_path = os.path.join(macro_file_path, "macro.json")
225
+
226
+ try:
227
+ with open(macro_file_path, "r") as f:
228
+ macro_data = json.load(f)
229
+
230
+ logger.info(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
231
+ return macro_data
232
+ except FileNotFoundError:
233
+ logger.error(f"❌ Macro file not found: {macro_file_path}")
234
+ return {}
235
+ except json.JSONDecodeError as e:
236
+ logger.error(f"❌ Error parsing macro file {macro_file_path}: {e}")
237
+ return {}
238
+
239
+ @staticmethod
240
+ def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
241
+ """
242
+ Get a summary of a macro sequence.
243
+
244
+ Args:
245
+ macro_data: The macro data dictionary
246
+
247
+ Returns:
248
+ Dictionary with statistics about the macro
249
+ """
250
+ if not macro_data or "actions" not in macro_data:
251
+ return {"error": "Invalid macro data"}
252
+
253
+ actions = macro_data["actions"]
254
+
255
+ # Count action types
256
+ action_types = {}
257
+ for action in actions:
258
+ action_type = action.get("action_type", "unknown")
259
+ action_types[action_type] = action_types.get(action_type, 0) + 1
260
+
261
+ # Calculate duration if timestamps are available
262
+ timestamps = [action.get("timestamp") for action in actions if action.get("timestamp")]
263
+ duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
264
+
265
+ return {
266
+ "version": macro_data.get("version", "unknown"),
267
+ "description": macro_data.get("description", "No description"),
268
+ "total_actions": len(actions),
269
+ "action_types": action_types,
270
+ "duration_seconds": round(duration, 2) if duration > 0 else None,
271
+ "timestamp": macro_data.get("timestamp", "unknown")
272
+ }
117
273
 
118
- return json_path
274
+ @staticmethod
275
+ def print_macro_summary(macro_file_path: str) -> None:
276
+ """
277
+ Print a summary of a macro sequence.
278
+
279
+ Args:
280
+ macro_file_path: Path to the macro JSON file or trajectory folder
281
+ """
282
+ macro_data = Trajectory.load_macro_sequence(macro_file_path)
283
+ if not macro_data:
284
+ print("❌ Could not load macro data")
285
+ return
286
+
287
+ summary = Trajectory.get_macro_summary(macro_data)
288
+
289
+ print("=== Macro Summary ===")
290
+ print(f"File: {macro_file_path}")
291
+ print(f"Version: {summary.get('version', 'unknown')}")
292
+ print(f"Description: {summary.get('description', 'No description')}")
293
+ print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
294
+ print(f"Total actions: {summary.get('total_actions', 0)}")
295
+ if summary.get('duration_seconds'):
296
+ print(f"Duration: {summary['duration_seconds']} seconds")
297
+ print("Action breakdown:")
298
+ for action_type, count in summary.get('action_types', {}).items():
299
+ print(f" - {action_type}: {count}")
300
+ print("=====================")
301
+
302
+ @staticmethod
303
+ def print_trajectory_folder_summary(trajectory_folder: str) -> None:
304
+ """
305
+ Print a comprehensive summary of a trajectory folder.
306
+
307
+ Args:
308
+ trajectory_folder: Path to the trajectory folder
309
+ """
310
+ folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
311
+
312
+ print("=== Trajectory Folder Summary ===")
313
+ print(f"Folder: {trajectory_folder}")
314
+ print(f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}")
315
+ print(f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}")
316
+ print(f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}")
317
+
318
+ if folder_data['macro_data']:
319
+ print("\n--- Macro Summary ---")
320
+ summary = Trajectory.get_macro_summary(folder_data['macro_data'])
321
+ print(f"Description: {summary.get('description', 'No description')}")
322
+ print(f"Total actions: {summary.get('total_actions', 0)}")
323
+ if summary.get('duration_seconds'):
324
+ print(f"Duration: {summary['duration_seconds']} seconds")
325
+ print("Action breakdown:")
326
+ for action_type, count in summary.get('action_types', {}).items():
327
+ print(f" - {action_type}: {count}")
328
+
329
+ if folder_data['trajectory_data']:
330
+ print(f"\n--- Trajectory Summary ---")
331
+ print(f"Total events: {len(folder_data['trajectory_data'])}")
332
+
333
+ print("=================================")
119
334
 
120
335
  def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
121
336
  """
@@ -181,4 +396,36 @@ class Trajectory:
181
396
  print(f"Execution steps: {stats['execution_steps']}")
182
397
  print(f"Successful executions: {stats['successful_executions']}")
183
398
  print(f"Failed executions: {stats['failed_executions']}")
184
- print("==========================")
399
+ print("==========================")
400
+
401
+
402
+ # Example usage:
403
+ """
404
+ # Save a trajectory with a specific goal (automatically creates folder structure)
405
+ trajectory = Trajectory(goal="Open settings and check battery level")
406
+ # ... add events and screenshots to trajectory ...
407
+ folder_path = trajectory.save_trajectory()
408
+
409
+ # Or update the goal later
410
+ trajectory.set_goal("Navigate to Settings and find device info")
411
+
412
+ # Load entire trajectory folder
413
+ folder_data = Trajectory.load_trajectory_folder(folder_path)
414
+ trajectory_events = folder_data['trajectory_data']
415
+ macro_actions = folder_data['macro_data']
416
+ gif_path = folder_data['gif_path']
417
+
418
+ # Load just the macro from folder
419
+ macro_data = Trajectory.load_macro_sequence(folder_path)
420
+
421
+ # Print summaries
422
+ Trajectory.print_trajectory_folder_summary(folder_path)
423
+ Trajectory.print_macro_summary(folder_path)
424
+
425
+ # Example folder structure created:
426
+ # trajectories/
427
+ # └── trajectory_20250108_143052/
428
+ # ├── trajectory.json # Full trajectory events
429
+ # ├── macro.json # Macro sequence with goal as description
430
+ # └── screenshots.gif # Screenshot animation
431
+ """
droidrun/cli/main.py CHANGED
@@ -9,10 +9,11 @@ import logging
9
9
  import warnings
10
10
  from contextlib import nullcontext
11
11
  from rich.console import Console
12
+ from adbutils import adb
12
13
  from droidrun.agent.droid import DroidAgent
13
14
  from droidrun.agent.utils.llm_picker import load_llm
14
- from droidrun.adb import DeviceManager
15
15
  from droidrun.tools import AdbTools, IOSTools
16
+ from droidrun.agent.context.personas import DEFAULT, BIG_AGENT
16
17
  from functools import wraps
17
18
  from droidrun.cli.logs import LogHandler
18
19
  from droidrun.telemetry import print_telemetry_message
@@ -21,7 +22,10 @@ from droidrun.portal import (
21
22
  enable_portal_accessibility,
22
23
  PORTAL_PACKAGE_NAME,
23
24
  ping_portal,
25
+ ping_portal_tcp,
26
+ ping_portal_content,
24
27
  )
28
+ from droidrun.macro.cli import macro_cli
25
29
 
26
30
  # Suppress all warnings
27
31
  warnings.filterwarnings("ignore")
@@ -29,7 +33,6 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
29
33
  os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
30
34
 
31
35
  console = Console()
32
- device_manager = DeviceManager()
33
36
 
34
37
 
35
38
  def configure_logging(goal: str, debug: bool):
@@ -38,7 +41,7 @@ def configure_logging(goal: str, debug: bool):
38
41
 
39
42
  handler = LogHandler(goal)
40
43
  handler.setFormatter(
41
- logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
44
+ logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
42
45
  if debug
43
46
  else logging.Formatter("%(message)s", "%H:%M:%S")
44
47
  )
@@ -47,6 +50,12 @@ def configure_logging(goal: str, debug: bool):
47
50
  logger.setLevel(logging.DEBUG if debug else logging.INFO)
48
51
  logger.propagate = False
49
52
 
53
+ if debug:
54
+ tools_logger = logging.getLogger("droidrun-tools")
55
+ tools_logger.addHandler(handler)
56
+ tools_logger.propagate = False
57
+ tools_logger.setLevel(logging.DEBUG if debug else logging.INFO)
58
+
50
59
  return handler
51
60
 
52
61
 
@@ -72,8 +81,10 @@ async def run_command(
72
81
  reflection: bool,
73
82
  tracing: bool,
74
83
  debug: bool,
75
- save_trajectory: bool = False,
84
+ use_tcp: bool,
85
+ save_trajectory: str = "none",
76
86
  ios: bool = False,
87
+ allow_drag: bool = False,
77
88
  **kwargs,
78
89
  ):
79
90
  """Run a command on your Android device using natural language."""
@@ -95,8 +106,8 @@ async def run_command(
95
106
  # Device setup
96
107
  if device is None and not ios:
97
108
  logger.info("🔍 Finding connected device...")
98
- device_manager = DeviceManager()
99
- devices = await device_manager.list_devices()
109
+
110
+ devices = adb.list()
100
111
  if not devices:
101
112
  raise ValueError("No connected devices found.")
102
113
  device = devices[0].serial
@@ -108,7 +119,16 @@ async def run_command(
108
119
  else:
109
120
  logger.info(f"📱 Using device: {device}")
110
121
 
111
- tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
122
+ tools = (
123
+ AdbTools(serial=device, use_tcp=use_tcp)
124
+ if not ios
125
+ else IOSTools(url=device)
126
+ )
127
+ # Set excluded tools based on CLI flags
128
+ excluded_tools = [] if allow_drag else ["drag"]
129
+
130
+ # Select personas based on --drag flag
131
+ personas = [BIG_AGENT] if allow_drag else [DEFAULT]
112
132
 
113
133
  # LLM setup
114
134
  log_handler.update_step("Initializing LLM...")
@@ -134,6 +154,8 @@ async def run_command(
134
154
  goal=command,
135
155
  llm=llm,
136
156
  tools=tools,
157
+ personas=personas,
158
+ excluded_tools=excluded_tools,
137
159
  max_steps=steps,
138
160
  timeout=1000,
139
161
  vision=vision,
@@ -237,11 +259,17 @@ class DroidRunCLI(click.Group):
237
259
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
238
260
  )
239
261
  @click.option(
240
- "--save-trajectory",
262
+ "--use-tcp",
241
263
  is_flag=True,
242
- help="Save agent trajectory to file",
264
+ help="Use TCP communication for device control",
243
265
  default=False,
244
266
  )
267
+ @click.option(
268
+ "--save-trajectory",
269
+ type=click.Choice(["none", "step", "action"]),
270
+ help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
271
+ default="none",
272
+ )
245
273
  @click.group(cls=DroidRunCLI)
246
274
  def cli(
247
275
  device: str | None,
@@ -256,7 +284,8 @@ def cli(
256
284
  reflection: bool,
257
285
  tracing: bool,
258
286
  debug: bool,
259
- save_trajectory: bool,
287
+ use_tcp: bool,
288
+ save_trajectory: str,
260
289
  ):
261
290
  """DroidRun - Control your Android device through LLM agents."""
262
291
  pass
@@ -311,10 +340,23 @@ def cli(
311
340
  @click.option(
312
341
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
313
342
  )
343
+ @click.option(
344
+ "--use-tcp",
345
+ is_flag=True,
346
+ help="Use TCP communication for device control",
347
+ default=False,
348
+ )
314
349
  @click.option(
315
350
  "--save-trajectory",
351
+ type=click.Choice(["none", "step", "action"]),
352
+ help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
353
+ default="none",
354
+ )
355
+ @click.option(
356
+ "--drag",
357
+ "allow_drag",
316
358
  is_flag=True,
317
- help="Save agent trajectory to file",
359
+ help="Enable drag tool",
318
360
  default=False,
319
361
  )
320
362
  @click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
@@ -332,7 +374,9 @@ def run(
332
374
  reflection: bool,
333
375
  tracing: bool,
334
376
  debug: bool,
335
- save_trajectory: bool,
377
+ use_tcp: bool,
378
+ save_trajectory: str,
379
+ allow_drag: bool,
336
380
  ios: bool,
337
381
  ):
338
382
  """Run a command on your Android device using natural language."""
@@ -350,18 +394,19 @@ def run(
350
394
  reflection,
351
395
  tracing,
352
396
  debug,
397
+ use_tcp,
353
398
  temperature=temperature,
354
399
  save_trajectory=save_trajectory,
400
+ allow_drag=allow_drag,
355
401
  ios=ios,
356
402
  )
357
403
 
358
404
 
359
405
  @cli.command()
360
- @coro
361
- async def devices():
406
+ def devices():
362
407
  """List connected Android devices."""
363
408
  try:
364
- devices = await device_manager.list_devices()
409
+ devices = adb.list()
365
410
  if not devices:
366
411
  console.print("[yellow]No devices connected.[/]")
367
412
  return
@@ -375,27 +420,24 @@ async def devices():
375
420
 
376
421
  @cli.command()
377
422
  @click.argument("serial")
378
- @click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
379
- @coro
380
- async def connect(serial: str, port: int):
423
+ def connect(serial: str):
381
424
  """Connect to a device over TCP/IP."""
382
425
  try:
383
- device = await device_manager.connect(serial, port)
384
- if device:
385
- console.print(f"[green]Successfully connected to {serial}:{port}[/]")
426
+ device = adb.connect(serial)
427
+ if device.count("already connected"):
428
+ console.print(f"[green]Successfully connected to {serial}[/]")
386
429
  else:
387
- console.print(f"[red]Failed to connect to {serial}:{port}[/]")
430
+ console.print(f"[red]Failed to connect to {serial}: {device}[/]")
388
431
  except Exception as e:
389
432
  console.print(f"[red]Error connecting to device: {e}[/]")
390
433
 
391
434
 
392
435
  @cli.command()
393
436
  @click.argument("serial")
394
- @coro
395
- async def disconnect(serial: str):
437
+ def disconnect(serial: str):
396
438
  """Disconnect from a device."""
397
439
  try:
398
- success = await device_manager.disconnect(serial)
440
+ success = adb.disconnect(serial, raise_error=True)
399
441
  if success:
400
442
  console.print(f"[green]Successfully disconnected from {serial}[/]")
401
443
  else:
@@ -406,16 +448,19 @@ async def disconnect(serial: str):
406
448
 
407
449
  @cli.command()
408
450
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
409
- @click.option("--path", help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.", default=None)
451
+ @click.option(
452
+ "--path",
453
+ help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.",
454
+ default=None,
455
+ )
410
456
  @click.option(
411
457
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
412
458
  )
413
- @coro
414
- async def setup(path: str | None, device: str | None, debug: bool):
459
+ def setup(path: str | None, device: str | None, debug: bool):
415
460
  """Install and enable the DroidRun Portal on a device."""
416
461
  try:
417
462
  if not device:
418
- devices = await device_manager.list_devices()
463
+ devices = adb.list()
419
464
  if not devices:
420
465
  console.print("[yellow]No devices connected.[/]")
421
466
  return
@@ -423,7 +468,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
423
468
  device = devices[0].serial
424
469
  console.print(f"[blue]Using device:[/] {device}")
425
470
 
426
- device_obj = await device_manager.get_device(device)
471
+ device_obj = adb.device(device)
427
472
  if not device_obj:
428
473
  console.print(
429
474
  f"[bold red]Error:[/] Could not get device object for {device}"
@@ -443,18 +488,20 @@ async def setup(path: str | None, device: str | None, debug: bool):
443
488
  return
444
489
 
445
490
  console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
446
- result = await device_obj.install_app(apk_path, True, True)
447
-
448
- if "Error" in result:
449
- console.print(f"[bold red]Installation failed:[/] {result}")
491
+ try:
492
+ device_obj.install(
493
+ apk_path, uninstall=True, flags=["-g"], silent=not debug
494
+ )
495
+ except Exception as e:
496
+ console.print(f"[bold red]Installation failed:[/] {e}")
450
497
  return
451
- else:
452
- console.print(f"[bold green]Installation successful![/]")
498
+
499
+ console.print(f"[bold green]Installation successful![/]")
453
500
 
454
501
  console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
455
502
 
456
503
  try:
457
- await enable_portal_accessibility(device_obj)
504
+ enable_portal_accessibility(device_obj)
458
505
 
459
506
  console.print("[green]Accessibility service enabled successfully![/]")
460
507
  console.print(
@@ -469,9 +516,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
469
516
  "[yellow]Opening accessibility settings for manual configuration...[/]"
470
517
  )
471
518
 
472
- await device_obj.shell(
473
- "am start -a android.settings.ACCESSIBILITY_SETTINGS"
474
- )
519
+ device_obj.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
475
520
 
476
521
  console.print(
477
522
  "\n[yellow]Please complete the following steps on your device:[/]"
@@ -500,19 +545,30 @@ async def setup(path: str | None, device: str | None, debug: bool):
500
545
 
501
546
  @cli.command()
502
547
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
548
+ @click.option(
549
+ "--use-tcp",
550
+ is_flag=True,
551
+ help="Use TCP communication for device control",
552
+ default=False,
553
+ )
503
554
  @click.option(
504
555
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
505
556
  )
506
- @coro
507
- async def ping(device: str | None, debug: bool):
557
+ def ping(device: str | None, use_tcp: bool, debug: bool):
508
558
  """Ping a device to check if it is ready and accessible."""
509
559
  try:
510
- device_obj = await device_manager.get_device(device)
560
+ device_obj = adb.device(device)
511
561
  if not device_obj:
512
562
  console.print(f"[bold red]Error:[/] Could not find device {device}")
513
563
  return
514
564
 
515
- await ping_portal(device_obj, debug)
565
+ ping_portal(device_obj, debug)
566
+
567
+ if use_tcp:
568
+ ping_portal_tcp(device_obj, debug)
569
+ else:
570
+ ping_portal_content(device_obj, debug)
571
+
516
572
  console.print(
517
573
  "[bold green]Portal is installed and accessible. You're good to go![/]"
518
574
  )
@@ -524,6 +580,10 @@ async def ping(device: str | None, debug: bool):
524
580
  traceback.print_exc()
525
581
 
526
582
 
583
+ # Add macro commands as a subgroup
584
+ cli.add_command(macro_cli, name="macro")
585
+
586
+
527
587
  if __name__ == "__main__":
528
588
  command = "Open the settings app"
529
589
  device = None
@@ -537,9 +597,11 @@ if __name__ == "__main__":
537
597
  reflection = False
538
598
  tracing = True
539
599
  debug = True
600
+ use_tcp = True
540
601
  base_url = None
541
602
  api_base = None
542
603
  ios = False
604
+ allow_drag = False
543
605
  run_command(
544
606
  command=command,
545
607
  device=device,
@@ -552,8 +614,10 @@ if __name__ == "__main__":
552
614
  reflection=reflection,
553
615
  tracing=tracing,
554
616
  debug=debug,
617
+ use_tcp=use_tcp,
555
618
  base_url=base_url,
556
619
  api_base=api_base,
557
620
  api_key=api_key,
621
+ allow_drag=allow_drag,
558
622
  ios=ios,
559
623
  )