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.
- droidrun/__init__.py +6 -2
- droidrun/agent/codeact/codeact_agent.py +20 -14
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -0
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/context/task_manager.py +8 -3
- droidrun/agent/droid/droid_agent.py +50 -16
- droidrun/agent/droid/events.py +1 -0
- droidrun/agent/planner/planner_agent.py +19 -14
- droidrun/agent/utils/chat_utils.py +1 -1
- droidrun/agent/utils/executer.py +17 -1
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +108 -44
- droidrun/macro/__init__.py +14 -0
- droidrun/macro/__main__.py +10 -0
- droidrun/macro/cli.py +228 -0
- droidrun/macro/replay.py +309 -0
- droidrun/portal.py +37 -22
- droidrun/telemetry/events.py +1 -1
- droidrun/telemetry/tracker.py +3 -2
- droidrun/tools/adb.py +641 -185
- droidrun/tools/ios.py +163 -163
- droidrun/tools/tools.py +60 -14
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/METADATA +20 -8
- droidrun-0.3.4.dist-info/RECORD +54 -0
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -345
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- droidrun-0.3.2.dist-info/RECORD +0 -53
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/WHEEL +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
DroidRun Macro Module - Record and replay UI automation sequences.
|
3
|
+
|
4
|
+
This module provides functionality to replay macro sequences that were
|
5
|
+
recorded during DroidAgent execution.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .replay import MacroPlayer, replay_macro_file, replay_macro_folder
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"MacroPlayer",
|
12
|
+
"replay_macro_file",
|
13
|
+
"replay_macro_folder"
|
14
|
+
]
|
droidrun/macro/cli.py
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
"""
|
2
|
+
Command-line interface for DroidRun macro replay.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import click
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from typing import Optional
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.table import Table
|
12
|
+
from droidrun.macro.replay import MacroPlayer, replay_macro_file, replay_macro_folder
|
13
|
+
from droidrun.agent.utils.trajectory import Trajectory
|
14
|
+
from adbutils import adb
|
15
|
+
|
16
|
+
console = Console()
|
17
|
+
|
18
|
+
|
19
|
+
def configure_logging(debug: bool = False):
|
20
|
+
"""Configure logging for the macro CLI."""
|
21
|
+
logger = logging.getLogger("droidrun-macro")
|
22
|
+
logger.handlers = []
|
23
|
+
|
24
|
+
handler = logging.StreamHandler()
|
25
|
+
|
26
|
+
if debug:
|
27
|
+
level = logging.DEBUG
|
28
|
+
formatter = logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
|
29
|
+
else:
|
30
|
+
level = logging.INFO
|
31
|
+
formatter = logging.Formatter("%(message)s", "%H:%M:%S")
|
32
|
+
|
33
|
+
handler.setFormatter(formatter)
|
34
|
+
logger.addHandler(handler)
|
35
|
+
logger.setLevel(level)
|
36
|
+
logger.propagate = False
|
37
|
+
|
38
|
+
return logger
|
39
|
+
|
40
|
+
|
41
|
+
@click.group()
|
42
|
+
def macro_cli():
|
43
|
+
"""Replay recorded automation sequences."""
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
@macro_cli.command()
|
48
|
+
@click.argument("path", type=click.Path(exists=True))
|
49
|
+
@click.option("--device", "-d", help="Device serial number", default=None)
|
50
|
+
@click.option("--delay", "-t", help="Delay between actions (seconds)", default=1.0, type=float)
|
51
|
+
@click.option("--start-from", "-s", help="Start from step number (1-based)", default=1, type=int)
|
52
|
+
@click.option("--max-steps", "-m", help="Maximum steps to execute", default=None, type=int)
|
53
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging", default=False)
|
54
|
+
@click.option("--dry-run", is_flag=True, help="Show actions without executing", default=False)
|
55
|
+
def replay(path: str, device: Optional[str], delay: float, start_from: int, max_steps: Optional[int], debug: bool, dry_run: bool):
|
56
|
+
"""Replay a macro from a file or trajectory folder."""
|
57
|
+
logger = configure_logging(debug)
|
58
|
+
|
59
|
+
logger.info("🎬 DroidRun Macro Replay")
|
60
|
+
|
61
|
+
# Convert start_from from 1-based to 0-based
|
62
|
+
start_from_zero = max(0, start_from - 1)
|
63
|
+
|
64
|
+
if device is None:
|
65
|
+
logger.info("🔍 Finding connected device...")
|
66
|
+
devices = adb.list()
|
67
|
+
if not devices:
|
68
|
+
raise ValueError("No connected devices found.")
|
69
|
+
device = devices[0].serial
|
70
|
+
logger.info(f"📱 Using device: {device}")
|
71
|
+
else:
|
72
|
+
logger.info(f"📱 Using device: {device}")
|
73
|
+
|
74
|
+
asyncio.run(_replay_async(path, device, delay, start_from_zero, max_steps, dry_run, logger))
|
75
|
+
|
76
|
+
|
77
|
+
async def _replay_async(path: str, device: str, delay: float, start_from: int, max_steps: Optional[int], dry_run: bool, logger: logging.Logger):
|
78
|
+
"""Async function to handle macro replay."""
|
79
|
+
try:
|
80
|
+
if os.path.isfile(path):
|
81
|
+
logger.info(f"📄 Loading macro from file: {path}")
|
82
|
+
player = MacroPlayer(device_serial=device, delay_between_actions=delay)
|
83
|
+
macro_data = player.load_macro_from_file(path)
|
84
|
+
elif os.path.isdir(path):
|
85
|
+
logger.info(f"📁 Loading macro from folder: {path}")
|
86
|
+
player = MacroPlayer(device_serial=device, delay_between_actions=delay)
|
87
|
+
macro_data = player.load_macro_from_folder(path)
|
88
|
+
else:
|
89
|
+
logger.error(f"❌ Invalid path: {path}")
|
90
|
+
return
|
91
|
+
|
92
|
+
if not macro_data:
|
93
|
+
logger.error("❌ Failed to load macro data")
|
94
|
+
return
|
95
|
+
|
96
|
+
# Show macro information
|
97
|
+
description = macro_data.get("description", "No description")
|
98
|
+
total_actions = macro_data.get("total_actions", 0)
|
99
|
+
version = macro_data.get("version", "unknown")
|
100
|
+
|
101
|
+
logger.info("📋 Macro Information:")
|
102
|
+
logger.info(f" Description: {description}")
|
103
|
+
logger.info(f" Version: {version}")
|
104
|
+
logger.info(f" Total actions: {total_actions}")
|
105
|
+
logger.info(f" Device: {device}")
|
106
|
+
logger.info(f" Delay between actions: {delay}s")
|
107
|
+
|
108
|
+
if start_from > 0:
|
109
|
+
logger.info(f" Starting from step: {start_from + 1}")
|
110
|
+
if max_steps:
|
111
|
+
logger.info(f" Maximum steps: {max_steps}")
|
112
|
+
|
113
|
+
if dry_run:
|
114
|
+
logger.info("🔍 DRY RUN MODE - Actions will be shown but not executed")
|
115
|
+
await _show_dry_run(macro_data, start_from, max_steps, logger)
|
116
|
+
else:
|
117
|
+
logger.info("▶️ Starting macro replay...")
|
118
|
+
success = await player.replay_macro(macro_data, start_from_step=start_from, max_steps=max_steps)
|
119
|
+
|
120
|
+
if success:
|
121
|
+
logger.info("🎉 Macro replay completed successfully!")
|
122
|
+
else:
|
123
|
+
logger.error("💥 Macro replay completed with errors")
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
logger.error(f"💥 Error: {e}")
|
127
|
+
if logger.isEnabledFor(logging.DEBUG):
|
128
|
+
import traceback
|
129
|
+
logger.debug(traceback.format_exc())
|
130
|
+
|
131
|
+
|
132
|
+
async def _show_dry_run(macro_data: dict, start_from: int, max_steps: Optional[int], logger: logging.Logger):
|
133
|
+
"""Show what actions would be executed in dry run mode."""
|
134
|
+
actions = macro_data.get("actions", [])
|
135
|
+
|
136
|
+
# Apply filters
|
137
|
+
if start_from > 0:
|
138
|
+
actions = actions[start_from:]
|
139
|
+
if max_steps:
|
140
|
+
actions = actions[:max_steps]
|
141
|
+
|
142
|
+
logger.info(f"📋 Found {len(actions)} actions to execute:")
|
143
|
+
|
144
|
+
table = Table(title="Actions to Execute")
|
145
|
+
table.add_column("Step", style="cyan")
|
146
|
+
table.add_column("Type", style="green")
|
147
|
+
table.add_column("Details", style="white")
|
148
|
+
table.add_column("Description", style="yellow")
|
149
|
+
|
150
|
+
for i, action in enumerate(actions, start=start_from + 1):
|
151
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
152
|
+
details = ""
|
153
|
+
|
154
|
+
if action_type == "tap":
|
155
|
+
x, y = action.get("x", 0), action.get("y", 0)
|
156
|
+
element_text = action.get("element_text", "")
|
157
|
+
details = f"({x}, {y}) - '{element_text}'"
|
158
|
+
elif action_type == "swipe":
|
159
|
+
start_x, start_y = action.get("start_x", 0), action.get("start_y", 0)
|
160
|
+
end_x, end_y = action.get("end_x", 0), action.get("end_y", 0)
|
161
|
+
details = f"({start_x}, {start_y}) → ({end_x}, {end_y})"
|
162
|
+
elif action_type == "input_text":
|
163
|
+
text = action.get("text", "")
|
164
|
+
details = f"'{text}'"
|
165
|
+
elif action_type == "key_press":
|
166
|
+
key_name = action.get("key_name", "UNKNOWN")
|
167
|
+
details = f"{key_name}"
|
168
|
+
|
169
|
+
description = action.get("description", "")
|
170
|
+
table.add_row(str(i), action_type, details, description[:50] + "..." if len(description) > 50 else description)
|
171
|
+
|
172
|
+
# Still use console for table display as it's structured data
|
173
|
+
console.print(table)
|
174
|
+
|
175
|
+
|
176
|
+
@macro_cli.command()
|
177
|
+
@click.argument("directory", type=click.Path(exists=True), default="trajectories")
|
178
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging", default=False)
|
179
|
+
def list(directory: str, debug: bool):
|
180
|
+
"""List available trajectory folders in a directory."""
|
181
|
+
logger = configure_logging(debug)
|
182
|
+
|
183
|
+
logger.info(f"📁 Scanning directory: {directory}")
|
184
|
+
|
185
|
+
try:
|
186
|
+
folders = []
|
187
|
+
for item in os.listdir(directory):
|
188
|
+
item_path = os.path.join(directory, item)
|
189
|
+
if os.path.isdir(item_path):
|
190
|
+
macro_file = os.path.join(item_path, "macro.json")
|
191
|
+
if os.path.exists(macro_file):
|
192
|
+
# Load macro info
|
193
|
+
try:
|
194
|
+
macro_data = Trajectory.load_macro_sequence(item_path)
|
195
|
+
description = macro_data.get("description", "No description")
|
196
|
+
total_actions = macro_data.get("total_actions", 0)
|
197
|
+
folders.append((item, description, total_actions))
|
198
|
+
except Exception as e:
|
199
|
+
logger.debug(f"Error loading macro from {item}: {e}")
|
200
|
+
folders.append((item, "Error loading", 0))
|
201
|
+
|
202
|
+
if not folders:
|
203
|
+
logger.info("📭 No trajectory folders found")
|
204
|
+
return
|
205
|
+
|
206
|
+
logger.info(f"🎯 Found {len(folders)} trajectory(s):")
|
207
|
+
|
208
|
+
table = Table(title=f"Available Trajectories in {directory}")
|
209
|
+
table.add_column("Folder", style="cyan")
|
210
|
+
table.add_column("Description", style="white")
|
211
|
+
table.add_column("Actions", style="green")
|
212
|
+
|
213
|
+
for folder, description, actions in sorted(folders):
|
214
|
+
table.add_row(folder, description[:80] + "..." if len(description) > 80 else description, str(actions))
|
215
|
+
|
216
|
+
# Still use console for table display as it's structured data
|
217
|
+
console.print(table)
|
218
|
+
logger.info(f"💡 Use 'droidrun macro replay {directory}/<folder>' to replay a trajectory")
|
219
|
+
|
220
|
+
except Exception as e:
|
221
|
+
logger.error(f"💥 Error: {e}")
|
222
|
+
if logger.isEnabledFor(logging.DEBUG):
|
223
|
+
import traceback
|
224
|
+
logger.debug(traceback.format_exc())
|
225
|
+
|
226
|
+
|
227
|
+
if __name__ == "__main__":
|
228
|
+
macro_cli()
|
droidrun/macro/replay.py
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
"""
|
2
|
+
Macro Replay Module - Replay recorded UI automation sequences.
|
3
|
+
|
4
|
+
This module provides functionality to load and replay macro JSON files
|
5
|
+
that were generated during DroidAgent trajectory recording.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import asyncio
|
10
|
+
import logging
|
11
|
+
import time
|
12
|
+
import os
|
13
|
+
from typing import Dict, List, Any, Optional
|
14
|
+
from droidrun.tools.adb import AdbTools
|
15
|
+
from droidrun.agent.utils.trajectory import Trajectory
|
16
|
+
|
17
|
+
logger = logging.getLogger("droidrun-macro")
|
18
|
+
|
19
|
+
|
20
|
+
class MacroPlayer:
|
21
|
+
"""
|
22
|
+
A class for loading and replaying DroidRun macro sequences.
|
23
|
+
|
24
|
+
This player can execute recorded UI actions (taps, swipes, text input, key presses)
|
25
|
+
on Android devices using AdbTools.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, device_serial: str = None, delay_between_actions: float = 1.0):
|
29
|
+
"""
|
30
|
+
Initialize the MacroPlayer.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
device_serial: Serial number of the target device. If None, will use first available device.
|
34
|
+
delay_between_actions: Delay in seconds between each action (default: 1.0s)
|
35
|
+
"""
|
36
|
+
self.device_serial = device_serial
|
37
|
+
self.delay_between_actions = delay_between_actions
|
38
|
+
self.adb_tools = None
|
39
|
+
|
40
|
+
def _initialize_tools(self) -> AdbTools:
|
41
|
+
"""Initialize ADB tools for the target device."""
|
42
|
+
if self.adb_tools is None:
|
43
|
+
self.adb_tools = AdbTools(serial=self.device_serial)
|
44
|
+
logger.info(f"🤖 Initialized ADB tools for device: {self.device_serial}")
|
45
|
+
return self.adb_tools
|
46
|
+
|
47
|
+
def load_macro_from_file(self, macro_file_path: str) -> Dict[str, Any]:
|
48
|
+
"""
|
49
|
+
Load macro data from a JSON file.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
macro_file_path: Path to the macro JSON file
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Dictionary containing the macro data
|
56
|
+
"""
|
57
|
+
return Trajectory.load_macro_sequence(macro_file_path)
|
58
|
+
|
59
|
+
def load_macro_from_folder(self, trajectory_folder: str) -> Dict[str, Any]:
|
60
|
+
"""
|
61
|
+
Load macro data from a trajectory folder.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
trajectory_folder: Path to the trajectory folder containing macro.json
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Dictionary containing the macro data
|
68
|
+
"""
|
69
|
+
return Trajectory.load_macro_sequence(trajectory_folder)
|
70
|
+
|
71
|
+
def replay_action(self, action: Dict[str, Any]) -> bool:
|
72
|
+
"""
|
73
|
+
Replay a single action.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
action: Action dictionary containing type and parameters
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
True if action was executed successfully, False otherwise
|
80
|
+
"""
|
81
|
+
tools = self._initialize_tools()
|
82
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
83
|
+
|
84
|
+
try:
|
85
|
+
|
86
|
+
if action_type == "start_app":
|
87
|
+
package = action.get("package")
|
88
|
+
activity = action.get("activity", None)
|
89
|
+
tools.start_app(package, activity)
|
90
|
+
return True
|
91
|
+
|
92
|
+
elif action_type == "tap":
|
93
|
+
x = action.get("x", 0)
|
94
|
+
y = action.get("y", 0)
|
95
|
+
element_text = action.get("element_text", "")
|
96
|
+
|
97
|
+
logger.info(f"🫰 Tapping at ({x}, {y}) - Element: '{element_text}'")
|
98
|
+
result = tools.tap_by_coordinates(x, y)
|
99
|
+
logger.debug(f" Result: {result}")
|
100
|
+
return True
|
101
|
+
|
102
|
+
elif action_type == "swipe":
|
103
|
+
start_x = action.get("start_x", 0)
|
104
|
+
start_y = action.get("start_y", 0)
|
105
|
+
end_x = action.get("end_x", 0)
|
106
|
+
end_y = action.get("end_y", 0)
|
107
|
+
duration_ms = action.get("duration_ms", 300)
|
108
|
+
|
109
|
+
logger.info(
|
110
|
+
f"👆 Swiping from ({start_x}, {start_y}) to ({end_x}, {end_y}) in {duration_ms} milliseconds"
|
111
|
+
)
|
112
|
+
result = tools.swipe(start_x, start_y, end_x, end_y, duration_ms)
|
113
|
+
logger.debug(f" Result: {result}")
|
114
|
+
return True
|
115
|
+
|
116
|
+
elif action_type == "drag":
|
117
|
+
start_x = action.get("start_x", 0)
|
118
|
+
start_y = action.get("start_y", 0)
|
119
|
+
end_x = action.get("end_x", 0)
|
120
|
+
end_y = action.get("end_y", 0)
|
121
|
+
duration_ms = action.get("duration_ms", 300)
|
122
|
+
|
123
|
+
logger.info(
|
124
|
+
f"👆 Dragging from ({start_x}, {start_y}) to ({end_x}, {end_y}) in {duration_ms} milliseconds"
|
125
|
+
)
|
126
|
+
result = tools.drag(start_x, start_y, end_x, end_y, duration_ms)
|
127
|
+
logger.debug(f" Result: {result}")
|
128
|
+
return True
|
129
|
+
|
130
|
+
elif action_type == "input_text":
|
131
|
+
text = action.get("text", "")
|
132
|
+
|
133
|
+
logger.info(f"⌨️ Inputting text: '{text}'")
|
134
|
+
result = tools.input_text(text)
|
135
|
+
logger.debug(f" Result: {result}")
|
136
|
+
return True
|
137
|
+
|
138
|
+
elif action_type == "key_press":
|
139
|
+
keycode = action.get("keycode", 0)
|
140
|
+
key_name = action.get("key_name", "UNKNOWN")
|
141
|
+
|
142
|
+
logger.info(f"🔘 Pressing key: {key_name} (keycode: {keycode})")
|
143
|
+
result = tools.press_key(keycode)
|
144
|
+
logger.debug(f" Result: {result}")
|
145
|
+
return True
|
146
|
+
|
147
|
+
elif action_type == "back":
|
148
|
+
logger.info(f"⬅️ Pressing back button")
|
149
|
+
result = tools.back()
|
150
|
+
logger.debug(f" Result: {result}")
|
151
|
+
return True
|
152
|
+
|
153
|
+
else:
|
154
|
+
logger.warning(f"⚠️ Unknown action type: {action_type}")
|
155
|
+
return False
|
156
|
+
|
157
|
+
except Exception as e:
|
158
|
+
logger.error(f"❌ Error executing action {action_type}: {e}")
|
159
|
+
return False
|
160
|
+
|
161
|
+
async def replay_macro(
|
162
|
+
self,
|
163
|
+
macro_data: Dict[str, Any],
|
164
|
+
start_from_step: int = 0,
|
165
|
+
max_steps: Optional[int] = None,
|
166
|
+
) -> bool:
|
167
|
+
"""
|
168
|
+
Replay a complete macro sequence.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
macro_data: Macro data dictionary loaded from JSON
|
172
|
+
start_from_step: Step number to start from (0-based, default: 0)
|
173
|
+
max_steps: Maximum number of steps to execute (default: all)
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
True if all actions were executed successfully, False otherwise
|
177
|
+
"""
|
178
|
+
if not macro_data or "actions" not in macro_data:
|
179
|
+
logger.error("❌ Invalid macro data - no actions found")
|
180
|
+
return False
|
181
|
+
|
182
|
+
actions = macro_data["actions"]
|
183
|
+
description = macro_data.get("description", "Unknown macro")
|
184
|
+
total_actions = len(actions)
|
185
|
+
|
186
|
+
# Apply start_from_step and max_steps filters
|
187
|
+
if start_from_step > 0:
|
188
|
+
actions = actions[start_from_step:]
|
189
|
+
logger.info(f"📍 Starting from step {start_from_step + 1}")
|
190
|
+
|
191
|
+
if max_steps is not None:
|
192
|
+
actions = actions[:max_steps]
|
193
|
+
logger.info(f"🎯 Limiting to {max_steps} steps")
|
194
|
+
|
195
|
+
logger.info(f"🎬 Starting macro replay: '{description}'")
|
196
|
+
logger.info(f"📊 Total actions to execute: {len(actions)} / {total_actions}")
|
197
|
+
|
198
|
+
success_count = 0
|
199
|
+
failed_count = 0
|
200
|
+
|
201
|
+
for i, action in enumerate(actions, start=start_from_step + 1):
|
202
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
203
|
+
description_text = action.get("description", "")
|
204
|
+
|
205
|
+
logger.info(f"\n📍 Step {i}/{total_actions}: {action_type}")
|
206
|
+
if description_text:
|
207
|
+
logger.info(f" Description: {description_text}")
|
208
|
+
|
209
|
+
# Execute the action
|
210
|
+
success = self.replay_action(action)
|
211
|
+
|
212
|
+
if success:
|
213
|
+
success_count += 1
|
214
|
+
logger.info(f" ✅ Action completed successfully")
|
215
|
+
else:
|
216
|
+
failed_count += 1
|
217
|
+
logger.error(f" ❌ Action failed")
|
218
|
+
|
219
|
+
# Wait between actions (except for the last one)
|
220
|
+
if i < len(actions):
|
221
|
+
logger.debug(f" ⏳ Waiting {self.delay_between_actions}s...")
|
222
|
+
await asyncio.sleep(self.delay_between_actions)
|
223
|
+
|
224
|
+
# Summary
|
225
|
+
total_executed = success_count + failed_count
|
226
|
+
success_rate = (
|
227
|
+
(success_count / total_executed * 100) if total_executed > 0 else 0
|
228
|
+
)
|
229
|
+
|
230
|
+
logger.info(f"\n🎉 Macro replay completed!")
|
231
|
+
logger.info(
|
232
|
+
f"📊 Success: {success_count}/{total_executed} ({success_rate:.1f}%)"
|
233
|
+
)
|
234
|
+
|
235
|
+
if failed_count > 0:
|
236
|
+
logger.warning(f"⚠️ Failed actions: {failed_count}")
|
237
|
+
|
238
|
+
return failed_count == 0
|
239
|
+
|
240
|
+
|
241
|
+
# Utility functions for convenience
|
242
|
+
|
243
|
+
|
244
|
+
async def replay_macro_file(
|
245
|
+
macro_file_path: str,
|
246
|
+
device_serial: str = None,
|
247
|
+
delay_between_actions: float = 1.0,
|
248
|
+
start_from_step: int = 0,
|
249
|
+
max_steps: Optional[int] = None,
|
250
|
+
) -> bool:
|
251
|
+
"""
|
252
|
+
Convenience function to replay a macro from a file.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
macro_file_path: Path to the macro JSON file
|
256
|
+
device_serial: Target device serial (optional)
|
257
|
+
delay_between_actions: Delay between actions in seconds
|
258
|
+
start_from_step: Step to start from (0-based)
|
259
|
+
max_steps: Maximum steps to execute
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
True if replay was successful, False otherwise
|
263
|
+
"""
|
264
|
+
player = MacroPlayer(
|
265
|
+
device_serial=device_serial, delay_between_actions=delay_between_actions
|
266
|
+
)
|
267
|
+
|
268
|
+
try:
|
269
|
+
macro_data = player.load_macro_from_file(macro_file_path)
|
270
|
+
return await player.replay_macro(
|
271
|
+
macro_data, start_from_step=start_from_step, max_steps=max_steps
|
272
|
+
)
|
273
|
+
except Exception as e:
|
274
|
+
logger.error(f"❌ Error replaying macro file {macro_file_path}: {e}")
|
275
|
+
return False
|
276
|
+
|
277
|
+
|
278
|
+
async def replay_macro_folder(
|
279
|
+
trajectory_folder: str,
|
280
|
+
device_serial: str = None,
|
281
|
+
delay_between_actions: float = 1.0,
|
282
|
+
start_from_step: int = 0,
|
283
|
+
max_steps: Optional[int] = None,
|
284
|
+
) -> bool:
|
285
|
+
"""
|
286
|
+
Convenience function to replay a macro from a trajectory folder.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
trajectory_folder: Path to the trajectory folder containing macro.json
|
290
|
+
device_serial: Target device serial (optional)
|
291
|
+
delay_between_actions: Delay between actions in seconds
|
292
|
+
start_from_step: Step to start from (0-based)
|
293
|
+
max_steps: Maximum steps to execute
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
True if replay was successful, False otherwise
|
297
|
+
"""
|
298
|
+
player = MacroPlayer(
|
299
|
+
device_serial=device_serial, delay_between_actions=delay_between_actions
|
300
|
+
)
|
301
|
+
|
302
|
+
try:
|
303
|
+
macro_data = player.load_macro_from_folder(trajectory_folder)
|
304
|
+
return await player.replay_macro(
|
305
|
+
macro_data, start_from_step=start_from_step, max_steps=max_steps
|
306
|
+
)
|
307
|
+
except Exception as e:
|
308
|
+
logger.error(f"❌ Error replaying macro folder {trajectory_folder}: {e}")
|
309
|
+
return False
|