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.
@@ -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
+ }