iflow-mcp_wegitor-logic_analyzer_mcp 0.1.0__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.
- iflow_mcp_wegitor_logic_analyzer_mcp-0.1.0.dist-info/METADATA +12 -0
- iflow_mcp_wegitor_logic_analyzer_mcp-0.1.0.dist-info/RECORD +15 -0
- iflow_mcp_wegitor_logic_analyzer_mcp-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_wegitor_logic_analyzer_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_wegitor_logic_analyzer_mcp-0.1.0.dist-info/top_level.txt +1 -0
- logic_analyzer_mcp/__init__.py +7 -0
- logic_analyzer_mcp/__main__.py +4 -0
- logic_analyzer_mcp/controllers/__init__.py +14 -0
- logic_analyzer_mcp/controllers/logic2_automation_controller.py +139 -0
- logic_analyzer_mcp/controllers/saleae_controller.py +929 -0
- logic_analyzer_mcp/controllers/saleae_parser_controller.py +548 -0
- logic_analyzer_mcp/logic_analyzer_mcp.py +62 -0
- logic_analyzer_mcp/mcp_tools.py +310 -0
- logic_analyzer_mcp/mcp_tools_experimental.py +636 -0
- logic_analyzer_mcp/saleae_manager.py +98 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any, Union
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from saleae import Saleae, Trigger, PerformanceOption
|
|
6
|
+
import psutil
|
|
7
|
+
import subprocess
|
|
8
|
+
# import pyautogui
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class SaleaeController:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
"""Initialize the Saleae controller."""
|
|
15
|
+
self.saleae = None
|
|
16
|
+
self.connected_device = None
|
|
17
|
+
self.sample_rate = None
|
|
18
|
+
self.active_channels = None
|
|
19
|
+
|
|
20
|
+
# Try to connect
|
|
21
|
+
if not self.connect():
|
|
22
|
+
logger.error("Failed to initialize Saleae connection")
|
|
23
|
+
# Don't raise exception here, let the caller handle it
|
|
24
|
+
|
|
25
|
+
def _find_saleae_software(self) -> Optional[str]:
|
|
26
|
+
"""Find Saleae Logic software installation path."""
|
|
27
|
+
# Common installation paths for Windows
|
|
28
|
+
possible_paths = [
|
|
29
|
+
# Logic 1 paths (older version)
|
|
30
|
+
os.path.expandvars(r"%ProgramFiles%\Saleae\Logic\Logic.exe"),
|
|
31
|
+
os.path.expandvars(r"%ProgramFiles(x86)%\Saleae\Logic\Logic.exe"),
|
|
32
|
+
os.path.expanduser(r"~\AppData\Local\Programs\Saleae\Logic\Logic.exe"),
|
|
33
|
+
# Legacy paths
|
|
34
|
+
os.path.expandvars(r"%ProgramFiles%\Logic\Logic.exe"),
|
|
35
|
+
os.path.expandvars(r"%ProgramFiles(x86)%\Logic\Logic.exe")
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for path in possible_paths:
|
|
39
|
+
if os.path.exists(path):
|
|
40
|
+
logger.info(f"Found Saleae Logic software at: {path}")
|
|
41
|
+
# Check permissions
|
|
42
|
+
try:
|
|
43
|
+
if os.access(path, os.R_OK):
|
|
44
|
+
logger.info(f" ✓ File is readable")
|
|
45
|
+
else:
|
|
46
|
+
logger.warning(f" ✗ File is not readable")
|
|
47
|
+
if os.access(path, os.X_OK):
|
|
48
|
+
logger.info(f" ✓ File is executable")
|
|
49
|
+
else:
|
|
50
|
+
logger.warning(f" ✗ File is not executable")
|
|
51
|
+
return path
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f" ✗ Error checking permissions: {str(e)}")
|
|
54
|
+
|
|
55
|
+
logger.error("Saleae Logic software not found in common installation paths")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def _launch_saleae_software(self) -> bool:
|
|
59
|
+
"""Launch Saleae Logic software if not running."""
|
|
60
|
+
try:
|
|
61
|
+
saleae_path = self._find_saleae_software()
|
|
62
|
+
if not saleae_path:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# Check if process is already running
|
|
66
|
+
for proc in psutil.process_iter(['name']):
|
|
67
|
+
if 'Logic.exe' in proc.info['name']:
|
|
68
|
+
logger.info("Saleae Logic software is already running")
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
# Launch the software with elevated privileges
|
|
72
|
+
logger.info(f"Launching Saleae Logic from: {saleae_path}")
|
|
73
|
+
if os.name == 'nt': # Windows
|
|
74
|
+
try:
|
|
75
|
+
process = subprocess.Popen(
|
|
76
|
+
[saleae_path],
|
|
77
|
+
shell=True,
|
|
78
|
+
creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Failed to launch with elevated privileges: {str(e)}")
|
|
82
|
+
# Try without elevated privileges
|
|
83
|
+
process = subprocess.Popen([saleae_path])
|
|
84
|
+
else:
|
|
85
|
+
process = subprocess.Popen([saleae_path])
|
|
86
|
+
|
|
87
|
+
# Wait for initial startup
|
|
88
|
+
time.sleep(5)
|
|
89
|
+
|
|
90
|
+
# Verify process is running
|
|
91
|
+
if process.poll() is None:
|
|
92
|
+
logger.info("Saleae Logic process is running")
|
|
93
|
+
# Wait for Logic to initialize
|
|
94
|
+
time.sleep(10) # Give more time for Logic to fully initialize
|
|
95
|
+
return True
|
|
96
|
+
else:
|
|
97
|
+
logger.error("Saleae Logic process exited immediately")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Failed to launch Saleae Logic software: {str(e)}")
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def connect(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Connect to Saleae Logic software.
|
|
107
|
+
If connection fails, it attempts to launch the software and retry connection.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
# First attempt to connect to an already running instance
|
|
111
|
+
self.saleae = Saleae()
|
|
112
|
+
logger.info("Successfully connected to an existing Saleae Logic instance.")
|
|
113
|
+
self.connected_device = self.saleae.get_active_device()
|
|
114
|
+
return True
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.info("Could not connect to existing Saleae Logic instance. Attempting to launch...")
|
|
117
|
+
|
|
118
|
+
# If connection fails, try launching the software
|
|
119
|
+
if self._launch_saleae_software():
|
|
120
|
+
logger.info("Saleae Logic software launched. Retrying connection...")
|
|
121
|
+
# Retry connection after launching
|
|
122
|
+
try:
|
|
123
|
+
# Give it a moment to initialize the server socket
|
|
124
|
+
time.sleep(2)
|
|
125
|
+
self.saleae = Saleae()
|
|
126
|
+
logger.info("Successfully connected to Saleae Logic after launching.")
|
|
127
|
+
self.connected_device = self.saleae.get_active_device()
|
|
128
|
+
return True
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Failed to connect to Saleae Logic after launching: {str(e)}")
|
|
131
|
+
return False
|
|
132
|
+
else:
|
|
133
|
+
logger.error("Failed to launch Saleae Logic software.")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def configure_capture(self,
|
|
137
|
+
digital_channels: List[int],
|
|
138
|
+
digital_sample_rate: int,
|
|
139
|
+
analog_channels: Optional[List[int]] = None,
|
|
140
|
+
analog_sample_rate: Optional[int] = None,
|
|
141
|
+
trigger_channel: Optional[int] = None,
|
|
142
|
+
trigger_type: Optional[str] = None) -> bool:
|
|
143
|
+
"""Configure capture settings."""
|
|
144
|
+
try:
|
|
145
|
+
# Set active channels
|
|
146
|
+
if self.connected_device.type not in ['LOGIC_4_DEVICE', 'LOGIC_DEVICE']:
|
|
147
|
+
self.saleae.set_active_channels(digital_channels, analog_channels)
|
|
148
|
+
|
|
149
|
+
# Set sample rate
|
|
150
|
+
if analog_channels and analog_sample_rate:
|
|
151
|
+
self.saleae.set_sample_rate_by_minimum(digital_sample_rate, analog_sample_rate)
|
|
152
|
+
else:
|
|
153
|
+
self.saleae.set_sample_rate_by_minimum(digital_sample_rate, 0)
|
|
154
|
+
|
|
155
|
+
# Set trigger if specified
|
|
156
|
+
if trigger_channel is not None and trigger_type is not None:
|
|
157
|
+
trigger_map = {
|
|
158
|
+
'high': Trigger.High,
|
|
159
|
+
'low': Trigger.Low,
|
|
160
|
+
'posedge': Trigger.Posedge,
|
|
161
|
+
'negedge': Trigger.Negedge,
|
|
162
|
+
'pospulse': Trigger.Pospulse,
|
|
163
|
+
'negpulse': Trigger.Negpulse
|
|
164
|
+
}
|
|
165
|
+
if trigger_type.lower() in trigger_map:
|
|
166
|
+
self.saleae.set_trigger_one_channel(trigger_channel, trigger_map[trigger_type.lower()])
|
|
167
|
+
|
|
168
|
+
# Store current configuration
|
|
169
|
+
self.active_channels = (digital_channels, analog_channels or [])
|
|
170
|
+
self.sample_rate = self.saleae.get_sample_rate()
|
|
171
|
+
|
|
172
|
+
return True
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to configure capture: {str(e)}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def parse_capture(self, file_path: str) -> Dict[str, Any]:
|
|
178
|
+
"""Parse a capture file and return its contents."""
|
|
179
|
+
try:
|
|
180
|
+
if not os.path.exists(file_path):
|
|
181
|
+
logger.error(f"Capture file not found: {file_path}")
|
|
182
|
+
return {"status": "error", "message": "File not found"}
|
|
183
|
+
|
|
184
|
+
# Check file extension
|
|
185
|
+
_, ext = os.path.splitext(file_path)
|
|
186
|
+
if ext.lower() == '.logicdata':
|
|
187
|
+
logger.info(f"Found Logic 1.x capture file: {file_path}")
|
|
188
|
+
# For Logic 1.x, we can't parse the file programmatically
|
|
189
|
+
# Return basic file info
|
|
190
|
+
return {
|
|
191
|
+
"status": "success",
|
|
192
|
+
"message": "File exists, please open in Logic software",
|
|
193
|
+
"file_type": "logicdata",
|
|
194
|
+
"file_path": file_path,
|
|
195
|
+
"file_size": os.path.getsize(file_path),
|
|
196
|
+
"created": time.ctime(os.path.getctime(file_path)),
|
|
197
|
+
"modified": time.ctime(os.path.getmtime(file_path))
|
|
198
|
+
}
|
|
199
|
+
else:
|
|
200
|
+
logger.error(f"Unsupported file format: {ext}")
|
|
201
|
+
return {
|
|
202
|
+
"status": "error",
|
|
203
|
+
"message": f"Unsupported file format: {ext}",
|
|
204
|
+
"file_path": file_path
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to parse capture: {str(e)}")
|
|
209
|
+
return {
|
|
210
|
+
"status": "error",
|
|
211
|
+
"message": str(e),
|
|
212
|
+
"file_path": file_path
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def load_capture(self, file_path: str) -> bool:
|
|
216
|
+
"""Load a capture file."""
|
|
217
|
+
try:
|
|
218
|
+
if not os.path.exists(file_path):
|
|
219
|
+
logger.error(f"Capture file not found: {file_path}")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
# Check file extension
|
|
223
|
+
_, ext = os.path.splitext(file_path)
|
|
224
|
+
if ext.lower() == '.logicdata':
|
|
225
|
+
logger.info(f"Found Logic 1.x capture file: {file_path}")
|
|
226
|
+
logger.info("Please open the file manually in Saleae Logic software")
|
|
227
|
+
return True
|
|
228
|
+
else:
|
|
229
|
+
logger.error(f"Unsupported file format: {ext}")
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to load capture: {str(e)}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def start_capture(self, duration_seconds: float) -> bool:
|
|
237
|
+
"""Start a capture for specified duration."""
|
|
238
|
+
try:
|
|
239
|
+
# Verify connection is still active
|
|
240
|
+
if not hasattr(self, 'saleae') or self.saleae is None:
|
|
241
|
+
logger.error("Saleae connection not initialized")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# Create a unique filename with timestamp
|
|
245
|
+
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
246
|
+
capture_file = os.path.join(os.getcwd(), f"capture_{timestamp}.logicdata")
|
|
247
|
+
|
|
248
|
+
# Set capture duration
|
|
249
|
+
logger.info(f"Setting capture duration to {duration_seconds} seconds...")
|
|
250
|
+
self.saleae.set_capture_seconds(duration_seconds)
|
|
251
|
+
|
|
252
|
+
# Start capture to file
|
|
253
|
+
logger.info(f"Starting capture, saving to {capture_file}...")
|
|
254
|
+
self.saleae.capture_to_file(capture_file)
|
|
255
|
+
|
|
256
|
+
# Wait for processing to complete
|
|
257
|
+
while not self.saleae.is_processing_complete():
|
|
258
|
+
time.sleep(0.1)
|
|
259
|
+
|
|
260
|
+
logger.info("Capture completed successfully")
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Failed to start capture: {str(e)}")
|
|
265
|
+
logger.error("Please ensure Saleae Logic software is running")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
def stop_capture(self) -> bool:
|
|
269
|
+
"""Stop the current capture."""
|
|
270
|
+
try:
|
|
271
|
+
return self.saleae.capture_stop()
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Failed to stop capture: {str(e)}")
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def save_capture(self, file_path: str) -> bool:
|
|
277
|
+
"""Save the current capture to file."""
|
|
278
|
+
try:
|
|
279
|
+
self.saleae.save_to_file(file_path)
|
|
280
|
+
return True
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f"Failed to save capture: {str(e)}")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def _convert_sal_to_logicdata(self, sal_file: str, logicdata_file: str) -> bool:
|
|
286
|
+
"""Convert .sal file to .logicdata format using Saleae API."""
|
|
287
|
+
try:
|
|
288
|
+
# Ensure paths are absolute and use forward slashes
|
|
289
|
+
sal_file = os.path.abspath(sal_file).replace('\\', '/')
|
|
290
|
+
logicdata_file = os.path.abspath(logicdata_file).replace('\\', '/')
|
|
291
|
+
|
|
292
|
+
# Ensure output directory exists
|
|
293
|
+
output_dir = os.path.dirname(logicdata_file)
|
|
294
|
+
if not os.path.exists(output_dir):
|
|
295
|
+
os.makedirs(output_dir)
|
|
296
|
+
|
|
297
|
+
logger.info(f"Converting {sal_file} to {logicdata_file}")
|
|
298
|
+
|
|
299
|
+
# Close any open files first
|
|
300
|
+
try:
|
|
301
|
+
self.saleae.close_all_tabs()
|
|
302
|
+
time.sleep(1) # Give it a moment to close
|
|
303
|
+
logger.info("Closed all open tabs")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.warning(f"Failed to close tabs: {e}")
|
|
306
|
+
|
|
307
|
+
# Open the .sal file
|
|
308
|
+
try:
|
|
309
|
+
logger.info("Attempting to load .sal file...")
|
|
310
|
+
self.saleae.load_from_file(sal_file)
|
|
311
|
+
logger.info("File load command sent")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to load .sal file: {e}")
|
|
314
|
+
raise RuntimeError(f"Failed to load .sal file: {e}")
|
|
315
|
+
|
|
316
|
+
# Wait for loading with timeout
|
|
317
|
+
load_timeout = 30 # seconds
|
|
318
|
+
start_time = time.time()
|
|
319
|
+
try:
|
|
320
|
+
while not self.saleae.is_processing_complete():
|
|
321
|
+
if time.time() - start_time > load_timeout:
|
|
322
|
+
raise TimeoutError("Loading .sal file timed out")
|
|
323
|
+
time.sleep(0.1)
|
|
324
|
+
logger.info("File loading completed")
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Error while waiting for file to load: {e}")
|
|
327
|
+
raise RuntimeError(f"Error while waiting for file to load: {e}")
|
|
328
|
+
|
|
329
|
+
# Get active channels
|
|
330
|
+
try:
|
|
331
|
+
active_channels = self.saleae.get_active_channels()
|
|
332
|
+
logger.info(f"Active channels: {active_channels}")
|
|
333
|
+
if not active_channels or (not active_channels[0] and not active_channels[1]):
|
|
334
|
+
raise ValueError("No active channels found in the file")
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(f"Failed to get active channels: {e}")
|
|
337
|
+
raise RuntimeError(f"Failed to get active channels: {e}")
|
|
338
|
+
|
|
339
|
+
# Export to .logicdata format
|
|
340
|
+
try:
|
|
341
|
+
logger.info("Starting export to .logicdata format...")
|
|
342
|
+
self.saleae.export_data2(
|
|
343
|
+
logicdata_file,
|
|
344
|
+
digital_channels=active_channels[0],
|
|
345
|
+
analog_channels=active_channels[1],
|
|
346
|
+
format='logicdata'
|
|
347
|
+
)
|
|
348
|
+
logger.info("Export command sent")
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to start export: {e}")
|
|
351
|
+
raise RuntimeError(f"Failed to start export: {e}")
|
|
352
|
+
|
|
353
|
+
# Wait for export with timeout
|
|
354
|
+
export_timeout = 30 # seconds
|
|
355
|
+
start_time = time.time()
|
|
356
|
+
try:
|
|
357
|
+
while not self.saleae.is_processing_complete():
|
|
358
|
+
if time.time() - start_time > export_timeout:
|
|
359
|
+
raise TimeoutError("Export to .logicdata timed out")
|
|
360
|
+
time.sleep(0.1)
|
|
361
|
+
logger.info("Export completed")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"Error while waiting for export to complete: {e}")
|
|
364
|
+
raise RuntimeError(f"Error while waiting for export to complete: {e}")
|
|
365
|
+
|
|
366
|
+
# Verify the converted file exists
|
|
367
|
+
if not os.path.exists(logicdata_file):
|
|
368
|
+
raise FileNotFoundError(f"Converted .logicdata file not created: {logicdata_file}")
|
|
369
|
+
|
|
370
|
+
# Verify file size
|
|
371
|
+
file_size = os.path.getsize(logicdata_file)
|
|
372
|
+
if file_size == 0:
|
|
373
|
+
raise ValueError(f"Converted file is empty: {logicdata_file}")
|
|
374
|
+
|
|
375
|
+
logger.info(f"Successfully converted .sal file to .logicdata format (size: {file_size} bytes)")
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"Failed to convert .sal file: {e}")
|
|
380
|
+
import traceback
|
|
381
|
+
error_details = traceback.format_exc()
|
|
382
|
+
logger.error(f"Full error details:\n{error_details}")
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
def export_data(self,
|
|
386
|
+
input_file: str,
|
|
387
|
+
output_file: str,
|
|
388
|
+
format: str = 'csv',
|
|
389
|
+
digital_channels: Optional[List[int]] = None,
|
|
390
|
+
analog_channels: Optional[List[int]] = None,
|
|
391
|
+
time_span: Optional[List[float]] = None) -> Dict[str, Any]:
|
|
392
|
+
"""Export capture data to specified format."""
|
|
393
|
+
try:
|
|
394
|
+
# Verify input file exists
|
|
395
|
+
if not os.path.exists(input_file):
|
|
396
|
+
logger.error(f"Input file not found: {input_file}")
|
|
397
|
+
return {
|
|
398
|
+
"status": "error",
|
|
399
|
+
"message": "Input file not found",
|
|
400
|
+
"details": f"File does not exist: {input_file}"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# Prepare paths
|
|
404
|
+
input_file = os.path.abspath(input_file).replace('\\', '/')
|
|
405
|
+
output_file = os.path.abspath(output_file).replace('\\', '/')
|
|
406
|
+
|
|
407
|
+
# Ensure we're connected to Logic
|
|
408
|
+
if not self.connect():
|
|
409
|
+
return {
|
|
410
|
+
"status": "error",
|
|
411
|
+
"message": "Failed to connect to Logic",
|
|
412
|
+
"details": "Please make sure Logic software is running and try again"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Handle .sal files by first exporting to .logicdata
|
|
416
|
+
if input_file.lower().endswith('.sal'):
|
|
417
|
+
logger.info(f"Converting .sal file to .logicdata format")
|
|
418
|
+
temp_logicdata = os.path.splitext(input_file)[0] + '.logicdata'
|
|
419
|
+
try:
|
|
420
|
+
# Check if file is accessible
|
|
421
|
+
if not os.access(input_file, os.R_OK):
|
|
422
|
+
raise PermissionError(f"No read permission for file: {input_file}")
|
|
423
|
+
|
|
424
|
+
# Check file size
|
|
425
|
+
file_size = os.path.getsize(input_file)
|
|
426
|
+
if file_size == 0:
|
|
427
|
+
raise ValueError(f"File is empty: {input_file}")
|
|
428
|
+
|
|
429
|
+
logger.info(f"File size: {file_size} bytes")
|
|
430
|
+
|
|
431
|
+
# Try to convert .sal to .logicdata using API
|
|
432
|
+
if not self._convert_sal_to_logicdata(input_file, temp_logicdata):
|
|
433
|
+
return {
|
|
434
|
+
"status": "error",
|
|
435
|
+
"message": "Failed to convert .sal file",
|
|
436
|
+
"details": "Failed to convert .sal file using Saleae API. Please make sure the file is a valid .sal file."
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Use the converted file
|
|
440
|
+
input_file = temp_logicdata
|
|
441
|
+
logger.info(f"Successfully converted to .logicdata format")
|
|
442
|
+
|
|
443
|
+
except PermissionError as e:
|
|
444
|
+
logger.error(f"Permission error: {e}")
|
|
445
|
+
return {
|
|
446
|
+
"status": "error",
|
|
447
|
+
"message": "Permission denied",
|
|
448
|
+
"details": str(e)
|
|
449
|
+
}
|
|
450
|
+
except ValueError as e:
|
|
451
|
+
logger.error(f"Invalid file: {e}")
|
|
452
|
+
return {
|
|
453
|
+
"status": "error",
|
|
454
|
+
"message": "Invalid file",
|
|
455
|
+
"details": str(e)
|
|
456
|
+
}
|
|
457
|
+
except TimeoutError as e:
|
|
458
|
+
logger.error(f"Operation timeout: {e}")
|
|
459
|
+
return {
|
|
460
|
+
"status": "error",
|
|
461
|
+
"message": "Operation timeout",
|
|
462
|
+
"details": str(e)
|
|
463
|
+
}
|
|
464
|
+
except FileNotFoundError as e:
|
|
465
|
+
logger.error(f"File not found: {e}")
|
|
466
|
+
return {
|
|
467
|
+
"status": "error",
|
|
468
|
+
"message": "File not found",
|
|
469
|
+
"details": str(e)
|
|
470
|
+
}
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.error(f"Failed to convert .sal file: {str(e)}")
|
|
473
|
+
import traceback
|
|
474
|
+
error_details = traceback.format_exc()
|
|
475
|
+
logger.error(f"Full error details:\n{error_details}")
|
|
476
|
+
return {
|
|
477
|
+
"status": "error",
|
|
478
|
+
"message": "Failed to convert .sal file",
|
|
479
|
+
"details": f"Error during conversion: {str(e)}\n{error_details}"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Ensure output directory exists
|
|
483
|
+
output_dir = os.path.dirname(output_file)
|
|
484
|
+
if not os.path.exists(output_dir):
|
|
485
|
+
os.makedirs(output_dir)
|
|
486
|
+
logger.info(f"Created output directory: {output_dir}")
|
|
487
|
+
|
|
488
|
+
# Load the file
|
|
489
|
+
logger.info(f"Loading capture file: {input_file}")
|
|
490
|
+
self.saleae.load_from_file(input_file)
|
|
491
|
+
while not self.saleae.is_processing_complete():
|
|
492
|
+
time.sleep(0.1)
|
|
493
|
+
|
|
494
|
+
# Get channels if not provided
|
|
495
|
+
if digital_channels is None or analog_channels is None:
|
|
496
|
+
active_channels = self.saleae.get_active_channels()
|
|
497
|
+
digital_channels = digital_channels or active_channels[0]
|
|
498
|
+
analog_channels = analog_channels or active_channels[1]
|
|
499
|
+
|
|
500
|
+
# Export data
|
|
501
|
+
logger.info(f"Exporting data to: {output_file}")
|
|
502
|
+
try:
|
|
503
|
+
self.saleae.export_data2(
|
|
504
|
+
output_file,
|
|
505
|
+
digital_channels=digital_channels,
|
|
506
|
+
analog_channels=analog_channels,
|
|
507
|
+
time_span=time_span,
|
|
508
|
+
format=format
|
|
509
|
+
)
|
|
510
|
+
# Wait for export to complete
|
|
511
|
+
export_timeout = 30 # seconds
|
|
512
|
+
start_time = time.time()
|
|
513
|
+
while not self.saleae.is_processing_complete():
|
|
514
|
+
if time.time() - start_time > export_timeout:
|
|
515
|
+
raise TimeoutError("Export operation timed out")
|
|
516
|
+
time.sleep(0.1)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(f"Export failed: {e}")
|
|
519
|
+
return {
|
|
520
|
+
"status": "error",
|
|
521
|
+
"message": "Failed to export data",
|
|
522
|
+
"details": str(e)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Check if the output file exists
|
|
526
|
+
if os.path.exists(output_file):
|
|
527
|
+
logger.info(f"Successfully exported data to {output_file}")
|
|
528
|
+
return {
|
|
529
|
+
"status": "success",
|
|
530
|
+
"message": f"Data exported to {output_file}",
|
|
531
|
+
"format": format,
|
|
532
|
+
"channels": {
|
|
533
|
+
"digital": digital_channels,
|
|
534
|
+
"analog": analog_channels
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else:
|
|
538
|
+
logger.error("Export file not created")
|
|
539
|
+
return {
|
|
540
|
+
"status": "error",
|
|
541
|
+
"message": "Failed to export data",
|
|
542
|
+
"details": "Export file not created"
|
|
543
|
+
}
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.error(f"Failed to export data: {e}")
|
|
546
|
+
return {
|
|
547
|
+
"status": "error",
|
|
548
|
+
"message": "Failed to export data",
|
|
549
|
+
"details": str(e)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
def get_device_info(self) -> Dict[str, Any]:
|
|
553
|
+
"""Get information about the connected device."""
|
|
554
|
+
try:
|
|
555
|
+
device = self.saleae.get_active_device()
|
|
556
|
+
digital_channels, analog_channels = self.saleae.get_active_channels()
|
|
557
|
+
sample_rate = self.saleae.get_sample_rate()
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
"device_type": device.type,
|
|
561
|
+
"device_name": device.name,
|
|
562
|
+
"device_id": device.id,
|
|
563
|
+
"active_digital_channels": digital_channels,
|
|
564
|
+
"active_analog_channels": analog_channels,
|
|
565
|
+
"sample_rate": sample_rate
|
|
566
|
+
}
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.error(f"Failed to get device info: {str(e)}")
|
|
569
|
+
return {}
|
|
570
|
+
|
|
571
|
+
def close(self):
|
|
572
|
+
"""Close the connection to Saleae Logic."""
|
|
573
|
+
try:
|
|
574
|
+
if self.saleae:
|
|
575
|
+
self.saleae.exit()
|
|
576
|
+
except Exception as e:
|
|
577
|
+
logger.error(f"Error closing connection: {str(e)}")
|
|
578
|
+
|
|
579
|
+
def get_digital_data(self,
|
|
580
|
+
capture_file: str,
|
|
581
|
+
channel: int = 0,
|
|
582
|
+
start_time: Optional[float] = None,
|
|
583
|
+
end_time: Optional[float] = None,
|
|
584
|
+
max_samples: Optional[int] = None) -> Dict[str, Any]:
|
|
585
|
+
"""Get digital data from a capture file."""
|
|
586
|
+
try:
|
|
587
|
+
# Load the capture file
|
|
588
|
+
logger.info(f"Loading capture file: {capture_file}")
|
|
589
|
+
self.saleae.load_from_file(capture_file)
|
|
590
|
+
|
|
591
|
+
# Wait for processing to complete
|
|
592
|
+
while not self.saleae.is_processing_complete():
|
|
593
|
+
time.sleep(0.1)
|
|
594
|
+
|
|
595
|
+
# Convert time span if provided
|
|
596
|
+
time_span = None
|
|
597
|
+
if start_time is not None and end_time is not None:
|
|
598
|
+
time_span = [start_time, end_time]
|
|
599
|
+
|
|
600
|
+
# Create a temporary CSV file
|
|
601
|
+
temp_csv = os.path.splitext(capture_file)[0] + "_temp.csv"
|
|
602
|
+
|
|
603
|
+
# Export digital data to CSV
|
|
604
|
+
logger.info("Exporting digital data to CSV...")
|
|
605
|
+
self.saleae.export_data2(
|
|
606
|
+
temp_csv,
|
|
607
|
+
digital_channels=[channel],
|
|
608
|
+
time_span=time_span,
|
|
609
|
+
format='csv'
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Wait for export to complete
|
|
613
|
+
while not self.saleae.is_processing_complete():
|
|
614
|
+
time.sleep(0.1)
|
|
615
|
+
|
|
616
|
+
# Read and parse the exported data
|
|
617
|
+
digital_data = []
|
|
618
|
+
with open(temp_csv, 'r') as f:
|
|
619
|
+
# Skip header
|
|
620
|
+
next(f)
|
|
621
|
+
for line in f:
|
|
622
|
+
timestamp, value = line.strip().split(',')
|
|
623
|
+
digital_data.append({
|
|
624
|
+
'time': float(timestamp),
|
|
625
|
+
'value': int(value)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
# Clean up temp file
|
|
629
|
+
os.remove(temp_csv)
|
|
630
|
+
|
|
631
|
+
# Apply max_samples if specified
|
|
632
|
+
if max_samples is not None and len(digital_data) > max_samples:
|
|
633
|
+
step = len(digital_data) // max_samples
|
|
634
|
+
digital_data = digital_data[::step]
|
|
635
|
+
|
|
636
|
+
logger.info("Successfully got digital data")
|
|
637
|
+
return {
|
|
638
|
+
"status": "success",
|
|
639
|
+
"message": "Successfully got digital data",
|
|
640
|
+
"data": digital_data
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
except Exception as e:
|
|
644
|
+
logger.error(f"Failed to get digital data: {str(e)}")
|
|
645
|
+
return {
|
|
646
|
+
"status": "error",
|
|
647
|
+
"message": f"Failed to get digital data: {str(e)}"
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
def get_digital_data_batch(self,
|
|
651
|
+
capture_file: str,
|
|
652
|
+
channels: List[int],
|
|
653
|
+
start_time: Optional[float] = None,
|
|
654
|
+
end_time: Optional[float] = None,
|
|
655
|
+
max_samples: Optional[int] = None) -> Dict[str, Any]:
|
|
656
|
+
"""Get digital data from multiple channels in a capture file."""
|
|
657
|
+
try:
|
|
658
|
+
# Load the capture file
|
|
659
|
+
logger.info(f"Loading capture file: {capture_file}")
|
|
660
|
+
self.saleae.load_from_file(capture_file)
|
|
661
|
+
|
|
662
|
+
# Wait for processing to complete
|
|
663
|
+
while not self.saleae.is_processing_complete():
|
|
664
|
+
time.sleep(0.1)
|
|
665
|
+
|
|
666
|
+
# Convert time span if provided
|
|
667
|
+
time_span = None
|
|
668
|
+
if start_time is not None and end_time is not None:
|
|
669
|
+
time_span = [start_time, end_time]
|
|
670
|
+
|
|
671
|
+
# Create a temporary CSV file
|
|
672
|
+
temp_csv = os.path.splitext(capture_file)[0] + "_temp.csv"
|
|
673
|
+
|
|
674
|
+
# Export digital data to CSV
|
|
675
|
+
logger.info("Exporting digital data to CSV...")
|
|
676
|
+
self.saleae.export_data2(
|
|
677
|
+
temp_csv,
|
|
678
|
+
digital_channels=channels,
|
|
679
|
+
time_span=time_span,
|
|
680
|
+
format='csv'
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Wait for export to complete
|
|
684
|
+
while not self.saleae.is_processing_complete():
|
|
685
|
+
time.sleep(0.1)
|
|
686
|
+
|
|
687
|
+
# Read and parse the exported data
|
|
688
|
+
channel_data = {}
|
|
689
|
+
with open(temp_csv, 'r') as f:
|
|
690
|
+
# Skip header
|
|
691
|
+
next(f)
|
|
692
|
+
for line in f:
|
|
693
|
+
timestamp, *values = line.strip().split(',')
|
|
694
|
+
for i, value in enumerate(values):
|
|
695
|
+
channel = channels[i]
|
|
696
|
+
if channel not in channel_data:
|
|
697
|
+
channel_data[channel] = []
|
|
698
|
+
channel_data[channel].append({
|
|
699
|
+
'time': float(timestamp),
|
|
700
|
+
'value': int(value)
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
# Clean up temp file
|
|
704
|
+
os.remove(temp_csv)
|
|
705
|
+
|
|
706
|
+
# Apply max_samples if specified
|
|
707
|
+
if max_samples is not None:
|
|
708
|
+
for channel in channel_data:
|
|
709
|
+
if len(channel_data[channel]) > max_samples:
|
|
710
|
+
step = len(channel_data[channel]) // max_samples
|
|
711
|
+
channel_data[channel] = channel_data[channel][::step]
|
|
712
|
+
|
|
713
|
+
logger.info("Successfully got digital data for all channels")
|
|
714
|
+
return {
|
|
715
|
+
"status": "success",
|
|
716
|
+
"message": "Successfully got digital data for all channels",
|
|
717
|
+
"channels": channel_data
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
except Exception as e:
|
|
721
|
+
logger.error(f"Failed to get digital data: {str(e)}")
|
|
722
|
+
return {
|
|
723
|
+
"status": "error",
|
|
724
|
+
"message": f"Failed to get digital data: {str(e)}"
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
def detect_protocols(self, file_path: str) -> Dict[str, Any]:
|
|
728
|
+
"""Detect protocols in a capture file."""
|
|
729
|
+
try:
|
|
730
|
+
if not os.path.exists(file_path):
|
|
731
|
+
logger.error(f"Capture file not found: {file_path}")
|
|
732
|
+
return {"status": "error", "message": "File not found"}
|
|
733
|
+
|
|
734
|
+
# Check file extension
|
|
735
|
+
_, ext = os.path.splitext(file_path)
|
|
736
|
+
if ext.lower() == '.logicdata':
|
|
737
|
+
logger.info(f"Found Logic 1.x capture file: {file_path}")
|
|
738
|
+
|
|
739
|
+
try:
|
|
740
|
+
# First, try to open the file in Logic
|
|
741
|
+
logger.info("Opening capture file in Logic...")
|
|
742
|
+
try:
|
|
743
|
+
# Try to open the file using the Saleae API
|
|
744
|
+
self.saleae.open_capture_file(file_path)
|
|
745
|
+
logger.info("Successfully opened capture file")
|
|
746
|
+
except Exception as e:
|
|
747
|
+
logger.warning(f"Could not open file via API: {str(e)}")
|
|
748
|
+
logger.info("Please open the file manually in Logic software")
|
|
749
|
+
return {
|
|
750
|
+
"status": "error",
|
|
751
|
+
"message": "Please open the file manually in Logic software",
|
|
752
|
+
"file_path": file_path
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
# Wait for file to load
|
|
756
|
+
time.sleep(2)
|
|
757
|
+
|
|
758
|
+
# Get available analyzers
|
|
759
|
+
analyzers = self.saleae.get_available_analyzers()
|
|
760
|
+
logger.info(f"Available analyzers: {analyzers}")
|
|
761
|
+
|
|
762
|
+
# Try to detect protocols
|
|
763
|
+
detected_protocols = []
|
|
764
|
+
for analyzer in analyzers:
|
|
765
|
+
try:
|
|
766
|
+
# Try to add analyzer
|
|
767
|
+
self.saleae.add_analyzer(analyzer)
|
|
768
|
+
logger.info(f"Added analyzer: {analyzer}")
|
|
769
|
+
detected_protocols.append(analyzer)
|
|
770
|
+
except Exception as e:
|
|
771
|
+
logger.warning(f"Failed to add analyzer {analyzer}: {str(e)}")
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
"status": "success",
|
|
775
|
+
"message": "Protocol detection completed",
|
|
776
|
+
"file_type": "logicdata",
|
|
777
|
+
"file_path": file_path,
|
|
778
|
+
"available_analyzers": analyzers,
|
|
779
|
+
"detected_protocols": detected_protocols
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
except Exception as e:
|
|
783
|
+
logger.error(f"Failed to detect protocols: {str(e)}")
|
|
784
|
+
return {
|
|
785
|
+
"status": "error",
|
|
786
|
+
"message": f"Failed to detect protocols: {str(e)}",
|
|
787
|
+
"file_path": file_path
|
|
788
|
+
}
|
|
789
|
+
else:
|
|
790
|
+
logger.error(f"Unsupported file format: {ext}")
|
|
791
|
+
return {
|
|
792
|
+
"status": "error",
|
|
793
|
+
"message": f"Unsupported file format: {ext}",
|
|
794
|
+
"file_path": file_path
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
logger.error(f"Failed to detect protocols: {str(e)}")
|
|
799
|
+
return {
|
|
800
|
+
"status": "error",
|
|
801
|
+
"message": str(e),
|
|
802
|
+
"file_path": file_path
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
def get_digital_data_mcp(self,
|
|
806
|
+
input_file: str,
|
|
807
|
+
digital_channels: Optional[List[int]] = None,
|
|
808
|
+
time_span: Optional[List[float]] = None) -> Dict[str, Any]:
|
|
809
|
+
"""Get digital data from a capture file."""
|
|
810
|
+
try:
|
|
811
|
+
# Verify input file exists
|
|
812
|
+
if not os.path.exists(input_file):
|
|
813
|
+
logger.error(f"Input file not found: {input_file}")
|
|
814
|
+
return {
|
|
815
|
+
"status": "error",
|
|
816
|
+
"message": "Input file not found",
|
|
817
|
+
"details": f"File does not exist: {input_file}"
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
# Verify file extension
|
|
821
|
+
if not input_file.lower().endswith(('.logicdata')):
|
|
822
|
+
logger.error(f"Invalid file extension: {input_file}")
|
|
823
|
+
return {
|
|
824
|
+
"status": "error",
|
|
825
|
+
"message": "Invalid file extension",
|
|
826
|
+
"details": "File must have .logicdata extension"
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
# Ensure we're connected to Logic
|
|
830
|
+
if not self.connect():
|
|
831
|
+
return {
|
|
832
|
+
"status": "error",
|
|
833
|
+
"message": "Failed to connect to Logic",
|
|
834
|
+
"details": "Please make sure Logic software is running and try again"
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
# First, try to open the file in Logic
|
|
838
|
+
logger.info(f"Opening capture file in Logic: {input_file}")
|
|
839
|
+
try:
|
|
840
|
+
# Convert path to forward slashes and make it absolute
|
|
841
|
+
input_file = os.path.abspath(input_file).replace('\\', '/')
|
|
842
|
+
|
|
843
|
+
logger.info(f"Loading file: {input_file}")
|
|
844
|
+
self.saleae.load_from_file(input_file)
|
|
845
|
+
logger.info("Successfully opened capture file")
|
|
846
|
+
|
|
847
|
+
except Exception as e:
|
|
848
|
+
error_type = type(e).__name__
|
|
849
|
+
error_msg = str(e)
|
|
850
|
+
logger.error(f"Failed to open file: {error_type}: {error_msg}")
|
|
851
|
+
return {
|
|
852
|
+
"status": "error",
|
|
853
|
+
"message": "Failed to open file",
|
|
854
|
+
"details": f"{error_type}: {error_msg}"
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
# Wait for file to load
|
|
858
|
+
logger.info("Waiting for file to load...")
|
|
859
|
+
time.sleep(2)
|
|
860
|
+
|
|
861
|
+
# Wait for processing to complete
|
|
862
|
+
try:
|
|
863
|
+
logger.info("Waiting for processing to complete...")
|
|
864
|
+
while not self.saleae.is_processing_complete():
|
|
865
|
+
time.sleep(0.1)
|
|
866
|
+
logger.info("Processing completed")
|
|
867
|
+
except Exception as e:
|
|
868
|
+
logger.error(f"Error waiting for processing: {str(e)}")
|
|
869
|
+
return {
|
|
870
|
+
"status": "error",
|
|
871
|
+
"message": "Failed to process file",
|
|
872
|
+
"details": str(e)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
# Get digital data
|
|
876
|
+
try:
|
|
877
|
+
logger.info("Getting digital data...")
|
|
878
|
+
# Create a temporary CSV file
|
|
879
|
+
temp_csv = os.path.splitext(input_file)[0] + "_temp.csv"
|
|
880
|
+
|
|
881
|
+
# Export digital data to CSV
|
|
882
|
+
self.saleae.export_data2(
|
|
883
|
+
temp_csv,
|
|
884
|
+
digital_channels=digital_channels,
|
|
885
|
+
time_span=time_span,
|
|
886
|
+
format='csv'
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Wait for export to complete
|
|
890
|
+
while not self.saleae.is_processing_complete():
|
|
891
|
+
time.sleep(0.1)
|
|
892
|
+
|
|
893
|
+
# Read and parse the exported data
|
|
894
|
+
digital_data = []
|
|
895
|
+
with open(temp_csv, 'r') as f:
|
|
896
|
+
# Skip header
|
|
897
|
+
next(f)
|
|
898
|
+
for line in f:
|
|
899
|
+
timestamp, value = line.strip().split(',')
|
|
900
|
+
digital_data.append({
|
|
901
|
+
'time': float(timestamp),
|
|
902
|
+
'value': int(value)
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
# Clean up temp file
|
|
906
|
+
os.remove(temp_csv)
|
|
907
|
+
|
|
908
|
+
logger.info("Successfully got digital data")
|
|
909
|
+
return {
|
|
910
|
+
"status": "success",
|
|
911
|
+
"message": "Successfully got digital data",
|
|
912
|
+
"data": digital_data
|
|
913
|
+
}
|
|
914
|
+
except Exception as e:
|
|
915
|
+
logger.error(f"Error getting digital data: {str(e)}")
|
|
916
|
+
return {
|
|
917
|
+
"status": "error",
|
|
918
|
+
"message": "Failed to get digital data",
|
|
919
|
+
"details": str(e)
|
|
920
|
+
}
|
|
921
|
+
except Exception as e:
|
|
922
|
+
error_type = type(e).__name__
|
|
923
|
+
error_msg = str(e)
|
|
924
|
+
logger.error(f"Failed to get digital data: {error_type}: {error_msg}")
|
|
925
|
+
return {
|
|
926
|
+
"status": "error",
|
|
927
|
+
"message": "Failed to get digital data",
|
|
928
|
+
"details": f"{error_type}: {error_msg}"
|
|
929
|
+
}
|