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,548 @@
1
+ from typing import List, Dict, Optional, Union, Any
2
+ from mcp.server.fastmcp import FastMCP
3
+ from saleae import Saleae
4
+ from saleae.automation import Manager, Capture
5
+ import time
6
+ import os
7
+ import logging
8
+ import json
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class SaleaeParserController:
13
+ """Controller for Saleae Logic capture file parsing using both python-saleae API and Logic 2.x Automation API."""
14
+
15
+ def __init__(self, mcp: FastMCP):
16
+ """
17
+ Initialize the Saleae parser controller.
18
+
19
+ Args:
20
+ mcp (FastMCP): MCP server instance
21
+ """
22
+ self.mcp = mcp
23
+ self.saleae = None
24
+ self.manager = None
25
+ self.max_retries = 3
26
+ self.retry_delay = 1 # seconds
27
+
28
+ # Try to initialize both APIs
29
+ try:
30
+ # Initialize python-saleae API
31
+ self.saleae = Saleae()
32
+ logger.info("Successfully initialized python-saleae API")
33
+
34
+ # Initialize Logic 2.x Automation API
35
+ self.manager = Manager.connect()
36
+ logger.info("Successfully initialized Logic 2.x Automation API")
37
+ except Exception as e:
38
+ logger.warning(f"Could not initialize Saleae APIs: {e}")
39
+ logger.warning("File parsing will be limited to offline mode")
40
+
41
+ def _check_file_format(self, file_path: str) -> str:
42
+ """Check if the file is a .sal or .logicdata file."""
43
+ if not file_path:
44
+ raise ValueError("No file path provided")
45
+
46
+ if not os.path.exists(file_path):
47
+ raise ValueError(f"File does not exist: {file_path}")
48
+
49
+ file_ext = os.path.splitext(file_path)[1].lower()
50
+ if file_ext not in ['.sal', '.logicdata']:
51
+ raise ValueError(f"Unsupported file format: {file_ext}. Only .sal and .logicdata files are supported.")
52
+
53
+ return 'sal' if file_ext == '.sal' else 'logicdata'
54
+
55
+ def _ensure_connection(self, file_format: str):
56
+ """Ensure connection to Logic software, launching if necessary."""
57
+ if file_format == 'sal':
58
+ if self.manager is None:
59
+ logger.warning("Logic 2.x Automation API is not available. Operation will be limited to offline mode.")
60
+ return
61
+ return
62
+
63
+ # For logicdata format
64
+ if self.saleae is None:
65
+ try:
66
+ self.saleae = Saleae()
67
+ logger.info("Successfully initialized python-saleae API")
68
+ except Exception as e:
69
+ logger.error(f"Failed to initialize python-saleae API: {e}")
70
+ return
71
+
72
+ retries = 0
73
+ while retries < self.max_retries:
74
+ try:
75
+ self.saleae.connect()
76
+ logger.info("Successfully connected to Logic software")
77
+ return
78
+ except Exception as e:
79
+ if "Could not connect to Logic software" in str(e):
80
+ try:
81
+ self.saleae.launch_logic()
82
+ time.sleep(self.retry_delay)
83
+ logger.info("Successfully launched Logic software")
84
+ return
85
+ except Exception as launch_error:
86
+ logger.error(f"Failed to launch Logic software: {launch_error}")
87
+ return
88
+ logger.error(f"Connection error: {e}")
89
+ retries += 1
90
+ if retries < self.max_retries:
91
+ time.sleep(self.retry_delay)
92
+
93
+ logger.error("Failed to connect to Logic software after maximum retries")
94
+
95
+ def parse_capture_file(self,
96
+ capture_file: Optional[str] = None,
97
+ data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
98
+ """
99
+ Initialize parser with a capture file or direct data and return basic information.
100
+
101
+ Args:
102
+ capture_file: Path to the capture file (optional)
103
+ data: Direct data dictionary with duration, digital_channels, and analog_channels (optional)
104
+
105
+ Returns:
106
+ Dict[str, Any]: Basic information about the capture file
107
+ """
108
+ try:
109
+ if not capture_file:
110
+ return {
111
+ "status": "error",
112
+ "message": "No capture file provided",
113
+ "details": "Either capture_file or data parameter must be provided"
114
+ }
115
+
116
+ # Check file format
117
+ file_format = self._check_file_format(capture_file)
118
+
119
+ # Ensure connection based on file format
120
+ self._ensure_connection(file_format)
121
+
122
+ if file_format == 'sal':
123
+ if not self.manager:
124
+ return {
125
+ "status": "error",
126
+ "message": "Logic 2.x Automation API is not available",
127
+ "details": "Please ensure Logic 2.x software is installed and running"
128
+ }
129
+
130
+ # Use Logic 2.x API to get file info
131
+ with self.manager.load_capture(capture_file) as capture:
132
+ return {
133
+ "status": "success",
134
+ "file_info": {
135
+ "path": capture_file,
136
+ "format": "Saleae Logic 2.x (.sal)",
137
+ "duration": capture.duration,
138
+ "digital_channels": list(range(capture.digital_channel_count)),
139
+ "analog_channels": list(range(capture.analog_channel_count)),
140
+ "digital_sample_rate": capture.digital_sample_rate,
141
+ "analog_sample_rate": capture.analog_sample_rate
142
+ }
143
+ }
144
+ else: # logicdata
145
+ if not self.saleae:
146
+ return {
147
+ "status": "error",
148
+ "message": "python-saleae API is not available",
149
+ "details": "Please ensure Logic software is installed and running"
150
+ }
151
+
152
+ # Use python-saleae API to get file info
153
+ self.saleae.load_from_file(capture_file)
154
+
155
+ # Wait for processing to complete
156
+ while not self.saleae.is_processing_complete():
157
+ time.sleep(0.1)
158
+
159
+ return {
160
+ "status": "success",
161
+ "file_info": {
162
+ "path": capture_file,
163
+ "format": "Saleae Logic 1.x (.logicdata)",
164
+ "duration": self.saleae.get_capture_seconds(),
165
+ "digital_channels": self.saleae.get_active_channels()[0],
166
+ "analog_channels": self.saleae.get_active_channels()[1],
167
+ "digital_sample_rate": self.saleae.get_sample_rate()[0],
168
+ "analog_sample_rate": self.saleae.get_sample_rate()[1]
169
+ }
170
+ }
171
+ except ValueError as e:
172
+ return {
173
+ "status": "error",
174
+ "message": str(e),
175
+ "details": "Only .sal and .logicdata files are supported"
176
+ }
177
+ except Exception as e:
178
+ logger.error(f"Error parsing file: {e}")
179
+ return {
180
+ "status": "error",
181
+ "message": "Failed to parse file",
182
+ "details": str(e)
183
+ }
184
+
185
+ def get_digital_data(self,
186
+ capture_file: Optional[str] = None,
187
+ data: Optional[List[Dict[str, Union[float, bool]]]] = None,
188
+ channel: int = 0,
189
+ start_time: Optional[float] = None,
190
+ end_time: Optional[float] = None) -> Dict[str, Any]:
191
+ """
192
+ Get digital data for a specific channel.
193
+
194
+ Args:
195
+ capture_file: Path to the capture file (optional)
196
+ data: Direct data list of transitions (optional)
197
+ channel: Channel number
198
+ start_time: Start time in seconds (optional)
199
+ end_time: End time in seconds (optional)
200
+ """
201
+ try:
202
+ if not capture_file:
203
+ return {
204
+ "status": "error",
205
+ "message": "No capture file provided",
206
+ "details": "Saleae API requires a capture file for data extraction"
207
+ }
208
+
209
+ # Check file format
210
+ file_format = self._check_file_format(capture_file)
211
+
212
+ # Ensure connection based on file format
213
+ self._ensure_connection(file_format)
214
+
215
+ if not self.saleae:
216
+ return {
217
+ "status": "error",
218
+ "message": "python-saleae API is not available",
219
+ "details": "Please ensure Logic software is installed and running"
220
+ }
221
+
222
+ # Use python-saleae API to get digital data
223
+ self.saleae.load_from_file(capture_file)
224
+
225
+ # Wait for processing to complete
226
+ while not self.saleae.is_processing_complete():
227
+ time.sleep(0.1)
228
+
229
+ # Export digital data for the specified channel
230
+ temp_file = "temp_digital_export.csv"
231
+ self.saleae.export_data2(
232
+ temp_file,
233
+ digital_channels=[channel],
234
+ format='csv',
235
+ csv_column_headers=True,
236
+ csv_timestamp='time_stamp',
237
+ csv_combined=True,
238
+ csv_row_per_change=True
239
+ )
240
+
241
+ # Wait for export to complete
242
+ while not self.saleae.is_processing_complete():
243
+ time.sleep(0.1)
244
+
245
+ # Read and parse the exported data
246
+ digital_data = []
247
+ with open(temp_file, 'r') as f:
248
+ # Skip header
249
+ next(f)
250
+ for line in f:
251
+ timestamp, value = line.strip().split(',')
252
+ digital_data.append({
253
+ 'time': float(timestamp),
254
+ 'value': int(value)
255
+ })
256
+
257
+ # Clean up temp file
258
+ os.remove(temp_file)
259
+
260
+ # Filter by time range if specified
261
+ if start_time is not None or end_time is not None:
262
+ digital_data = [
263
+ point for point in digital_data
264
+ if (start_time is None or point['time'] >= start_time) and
265
+ (end_time is None or point['time'] <= end_time)
266
+ ]
267
+
268
+ return {
269
+ "status": "success",
270
+ "data": digital_data
271
+ }
272
+ except ValueError as e:
273
+ return {
274
+ "status": "error",
275
+ "message": str(e),
276
+ "details": "Only .sal and .logicdata files are supported"
277
+ }
278
+ except Exception as e:
279
+ logger.error(f"Error getting digital data: {e}")
280
+ return {
281
+ "status": "error",
282
+ "message": "Failed to get digital data",
283
+ "details": str(e)
284
+ }
285
+
286
+ def get_analog_data(self,
287
+ capture_file: Optional[str] = None,
288
+ data: Optional[List[Dict[str, Union[float, float]]]] = None,
289
+ channel: int = 0,
290
+ start_time: Optional[float] = None,
291
+ end_time: Optional[float] = None) -> Dict[str, Any]:
292
+ """
293
+ Get analog data for a specific channel.
294
+
295
+ Args:
296
+ capture_file: Path to the capture file (optional)
297
+ data: Direct data list of samples (optional)
298
+ channel: Channel number
299
+ start_time: Start time in seconds (optional)
300
+ end_time: End time in seconds (optional)
301
+ """
302
+ try:
303
+ if not capture_file:
304
+ return {
305
+ "status": "error",
306
+ "message": "No capture file provided",
307
+ "details": "Saleae API requires a capture file for data extraction"
308
+ }
309
+
310
+ # Check file format
311
+ file_format = self._check_file_format(capture_file)
312
+
313
+ # Ensure connection based on file format
314
+ self._ensure_connection(file_format)
315
+
316
+ if not self.saleae:
317
+ return {
318
+ "status": "error",
319
+ "message": "python-saleae API is not available",
320
+ "details": "Please ensure Logic software is installed and running"
321
+ }
322
+
323
+ # Use python-saleae API to get analog data
324
+ self.saleae.load_from_file(capture_file)
325
+
326
+ # Wait for processing to complete
327
+ while not self.saleae.is_processing_complete():
328
+ time.sleep(0.1)
329
+
330
+ # Export analog data for the specified channel
331
+ temp_file = "temp_analog_export.csv"
332
+ self.saleae.export_data2(
333
+ temp_file,
334
+ analog_channels=[channel],
335
+ format='csv',
336
+ csv_column_headers=True,
337
+ csv_timestamp='time_stamp',
338
+ csv_combined=True,
339
+ csv_row_per_change=True
340
+ )
341
+
342
+ # Wait for export to complete
343
+ while not self.saleae.is_processing_complete():
344
+ time.sleep(0.1)
345
+
346
+ # Read and parse the exported data
347
+ analog_data = []
348
+ with open(temp_file, 'r') as f:
349
+ # Skip header
350
+ next(f)
351
+ for line in f:
352
+ timestamp, value = line.strip().split(',')
353
+ analog_data.append({
354
+ 'time': float(timestamp),
355
+ 'value': float(value)
356
+ })
357
+
358
+ # Clean up temp file
359
+ os.remove(temp_file)
360
+
361
+ # Filter by time range if specified
362
+ if start_time is not None or end_time is not None:
363
+ analog_data = [
364
+ point for point in analog_data
365
+ if (start_time is None or point['time'] >= start_time) and
366
+ (end_time is None or point['time'] <= end_time)
367
+ ]
368
+
369
+ return {
370
+ "status": "success",
371
+ "data": analog_data
372
+ }
373
+ except ValueError as e:
374
+ return {
375
+ "status": "error",
376
+ "message": str(e),
377
+ "details": "Only .sal and .logicdata files are supported"
378
+ }
379
+ except Exception as e:
380
+ logger.error(f"Error getting analog data: {e}")
381
+ return {
382
+ "status": "error",
383
+ "message": "Failed to get analog data",
384
+ "details": str(e)
385
+ }
386
+
387
+ def export_data(self,
388
+ capture_file: Optional[str] = None,
389
+ data: Optional[Dict[str, Any]] = None,
390
+ output_file: str = "exported_data.csv",
391
+ format: str = "csv",
392
+ digital_channels: Optional[List[int]] = None,
393
+ analog_channels: Optional[List[int]] = None,
394
+ start_time: Optional[float] = None,
395
+ end_time: Optional[float] = None) -> Dict[str, Any]:
396
+ """
397
+ Export data from a capture file.
398
+
399
+ Args:
400
+ capture_file: Path to the capture file (optional)
401
+ data: Direct data dictionary (optional)
402
+ output_file: Path to save the exported data
403
+ format: Export format ('csv', 'binary', 'vcd', 'matlab')
404
+ digital_channels: List of digital channels to export
405
+ analog_channels: List of analog channels to export
406
+ start_time: Start time in seconds (optional)
407
+ end_time: End time in seconds (optional)
408
+ """
409
+ try:
410
+ if not capture_file:
411
+ return {
412
+ "status": "error",
413
+ "message": "No capture file provided",
414
+ "details": "Saleae API requires a capture file for data export"
415
+ }
416
+
417
+ # Check file format
418
+ file_format = self._check_file_format(capture_file)
419
+
420
+ # Ensure connection based on file format
421
+ self._ensure_connection(file_format)
422
+
423
+ if not self.saleae:
424
+ return {
425
+ "status": "error",
426
+ "message": "python-saleae API is not available",
427
+ "details": "Please ensure Logic software is installed and running"
428
+ }
429
+
430
+ # Use python-saleae API to export data
431
+ self.saleae.load_from_file(capture_file)
432
+
433
+ # Wait for processing to complete
434
+ while not self.saleae.is_processing_complete():
435
+ time.sleep(0.1)
436
+
437
+ # Export data using export_data2
438
+ self.saleae.export_data2(
439
+ output_file,
440
+ digital_channels=digital_channels,
441
+ analog_channels=analog_channels,
442
+ format=format,
443
+ csv_column_headers=True,
444
+ csv_timestamp='time_stamp',
445
+ csv_combined=True,
446
+ csv_row_per_change=True
447
+ )
448
+
449
+ # Wait for export to complete
450
+ while not self.saleae.is_processing_complete():
451
+ time.sleep(0.1)
452
+
453
+ return {
454
+ "status": "success",
455
+ "message": f"Data exported to {output_file}",
456
+ "format": format,
457
+ "channels": {
458
+ "digital": digital_channels,
459
+ "analog": analog_channels
460
+ }
461
+ }
462
+ except ValueError as e:
463
+ return {
464
+ "status": "error",
465
+ "message": str(e),
466
+ "details": "Only .sal and .logicdata files are supported"
467
+ }
468
+ except ConnectionError as e:
469
+ return {
470
+ "status": "error",
471
+ "message": "Failed to connect to Logic software",
472
+ "details": str(e)
473
+ }
474
+ except TimeoutError as e:
475
+ return {
476
+ "status": "error",
477
+ "message": "Operation timed out",
478
+ "details": str(e)
479
+ }
480
+ except IOError as e:
481
+ return {
482
+ "status": "error",
483
+ "message": "File I/O error",
484
+ "details": str(e)
485
+ }
486
+ except Exception as e:
487
+ logger.error(f"Error exporting data: {e}")
488
+ return {
489
+ "status": "error",
490
+ "message": "Failed to export data",
491
+ "details": str(e)
492
+ }
493
+
494
+ def get_sample_rate(self,
495
+ capture_file: Optional[str] = None,
496
+ sample_rate: Optional[float] = None,
497
+ channel: int = 0) -> Dict[str, Any]:
498
+ """
499
+ Get the sample rate for a specific channel.
500
+
501
+ Args:
502
+ capture_file: Path to the capture file (optional)
503
+ sample_rate: Direct sample rate value (optional)
504
+ channel: Channel number
505
+ """
506
+ try:
507
+ if not capture_file:
508
+ return {
509
+ "status": "error",
510
+ "message": "No capture file provided",
511
+ "details": "Saleae API requires a capture file to get sample rate"
512
+ }
513
+
514
+ if not self.saleae:
515
+ return {
516
+ "status": "error",
517
+ "message": "Saleae software is not available",
518
+ "details": "Please ensure Saleae Logic software is installed and running"
519
+ }
520
+
521
+ # Connect to Saleae software
522
+ self._ensure_connection('logicdata')
523
+
524
+ # Load the capture file
525
+ self.saleae.load_from_file(capture_file)
526
+
527
+ # Wait for processing to complete
528
+ while not self.saleae.is_processing_complete():
529
+ time.sleep(0.1)
530
+
531
+ # Get sample rate based on channel type
532
+ digital_channels, analog_channels = self.saleae.get_active_channels()
533
+ if channel < len(digital_channels):
534
+ sample_rate = self.saleae.get_sample_rate()[0] # Digital sample rate
535
+ else:
536
+ sample_rate = self.saleae.get_sample_rate()[1] # Analog sample rate
537
+
538
+ return {
539
+ "status": "success",
540
+ "sample_rate": sample_rate
541
+ }
542
+ except Exception as e:
543
+ logger.error(f"Error getting sample rate: {e}")
544
+ return {
545
+ "status": "error",
546
+ "message": "Failed to get sample rate",
547
+ "details": str(e)
548
+ }
@@ -0,0 +1,62 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import argparse
5
+ from typing import Optional
6
+
7
+ # Add necessary paths for imports
8
+ script_dir = os.path.dirname(os.path.abspath(__file__))
9
+ sys.path.insert(0, script_dir) # Add script directory to path
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+ from controllers.logic2_automation_controller import Logic2AutomationController
13
+ from mcp import StdioServerParameters
14
+
15
+ # Import controllers
16
+ from controllers.saleae_parser_controller import SaleaeParserController
17
+ from mcp_tools import setup_mcp_tools
18
+
19
+ def main(enable_logic2: Optional[bool] = None):
20
+ """Start the MCP server. If enable_logic2 is None, CLI args/env determine it."""
21
+ # Setup logging
22
+ logging.basicConfig(
23
+ level=logging.WARNING,
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Determine enable_logic2 from args if not explicitly provided
29
+ if enable_logic2 is None:
30
+ parser = argparse.ArgumentParser(add_help=False)
31
+ parser.add_argument('--logic2', action='store_true', help='Enable Logic2 experimental MCP tools')
32
+ # parse only known args, leave others untouched
33
+ args, _ = parser.parse_known_args()
34
+ enable_logic2 = bool(args.logic2) or (os.environ.get("LOGIC2") and str(os.environ.get("LOGIC2")).lower() in ("1", "true", "yes"))
35
+
36
+ logger.info("Starting MCP server for Logic 2...")
37
+
38
+ try:
39
+ # Create MCP server
40
+ mcp = FastMCP("Logic 2 Control")
41
+
42
+ # Initialize Logic 2 automation controller
43
+ controller = Logic2AutomationController(manager=None) # Manager will be initialized in the controller
44
+
45
+ # Setup MCP tools (pass enable_logic2)
46
+ setup_mcp_tools(mcp, controller, enable_logic2=enable_logic2)
47
+
48
+ # Setup parser controller
49
+ logger.info("Initializing SaleaeParserController...")
50
+ parser_controller = SaleaeParserController(mcp)
51
+ logger.info("SaleaeParserController initialized successfully.")
52
+
53
+ # Run MCP server
54
+ logger.info("Starting MCP server...")
55
+ mcp.run()
56
+
57
+ except Exception as e:
58
+ logger.error(f"Error running MCP server: {e}")
59
+ raise
60
+
61
+ if __name__ == "__main__":
62
+ main()