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,636 @@
1
+ from mcp import types
2
+ from mcp.server.fastmcp import FastMCP, Context
3
+ from typing import Optional, Dict, Any, List, Union
4
+ from saleae.automation import DeviceType
5
+ from saleae import Saleae
6
+ import os
7
+ import json
8
+ import time
9
+ import logging
10
+
11
+ # Use shared saleae manager for instance creation/caching
12
+ from logic_analyzer_mcp.saleae_manager import get_saleae
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Try to import DeviceType if available
17
+ try:
18
+ from saleae.automation import DeviceType
19
+ except Exception:
20
+ DeviceType = None
21
+
22
+ def setup_mcp_tools_experimental(mcp: FastMCP, controller=None) -> None:
23
+
24
+ controller_instance = controller
25
+
26
+ # if controller is not None:
27
+ @mcp.tool("create_device_config")
28
+ def create_device_config(ctx: Context,
29
+ name: str,
30
+ digital_channels: List[int],
31
+ digital_sample_rate: int,
32
+ analog_channels: Optional[List[int]] = None,
33
+ analog_sample_rate: Optional[int] = None,
34
+ digital_threshold_volts: Optional[float] = None) -> Dict[str, Any]:
35
+ """Create a new device configuration for Saleae Logic 2."""
36
+ try:
37
+ config_name = controller.create_device_config(
38
+ name=name,
39
+ digital_channels=digital_channels,
40
+ digital_sample_rate=digital_sample_rate,
41
+ analog_channels=analog_channels,
42
+ analog_sample_rate=analog_sample_rate,
43
+ digital_threshold_volts=digital_threshold_volts
44
+ )
45
+ return {"status": "success", "message": f"Created device configuration: {config_name}"}
46
+ except Exception as e:
47
+ return {"status": "error", "message": f"Failed to create device configuration: {str(e)}"}
48
+
49
+ @mcp.tool("create_capture_config")
50
+ def create_capture_config(ctx: Context,
51
+ name: str,
52
+ duration_seconds: float,
53
+ buffer_size_megabytes: Optional[int] = None) -> Dict[str, Any]:
54
+ """Create a new capture configuration for Saleae Logic 2."""
55
+ try:
56
+ config_name = controller.create_capture_config(
57
+ name=name,
58
+ duration_seconds=duration_seconds,
59
+ buffer_size_megabytes=buffer_size_megabytes
60
+ )
61
+ return {"status": "success", "message": f"Created capture configuration: {config_name}"}
62
+ except Exception as e:
63
+ return {"status": "error", "message": f"Failed to create capture configuration: {str(e)}"}
64
+
65
+ @mcp.tool("get_available_devices")
66
+ def get_available_devices(ctx: Context) -> Dict[str, Any]:
67
+ """Get list of available Saleae Logic devices."""
68
+ try:
69
+ devices = controller.get_available_devices()
70
+ return {"status": "success", "devices": devices}
71
+ except Exception as e:
72
+ return {"status": "error", "message": f"Failed to get available devices: {str(e)}"}
73
+
74
+ @mcp.tool("find_device_by_type")
75
+ def find_device_by_type(ctx: Context, device_type: str) -> Dict[str, Any]:
76
+ """Find a Saleae Logic device by its type."""
77
+ try:
78
+ device = controller.find_device_by_type(DeviceType[device_type])
79
+ if device:
80
+ return {"status": "success", "device": device}
81
+ return {"status": "error", "message": f"No device found of type {device_type}"}
82
+ except ValueError as e:
83
+ return {"status": "error", "message": str(e)}
84
+ except Exception as e:
85
+ return {"status": "error", "message": f"Failed to find device: {str(e)}"}
86
+
87
+ @mcp.tool("list_device_configs")
88
+ def list_device_configs(ctx: Context) -> Dict[str, Any]:
89
+ """List all available device configurations."""
90
+ try:
91
+ configs = controller.list_device_configs()
92
+ return {"status": "success", "configurations": configs}
93
+ except Exception as e:
94
+ return {"status": "error", "message": f"Failed to list device configurations: {str(e)}"}
95
+
96
+ @mcp.tool("list_capture_configs")
97
+ def list_capture_configs(ctx: Context) -> Dict[str, Any]:
98
+ """List all available capture configurations."""
99
+ try:
100
+ configs = controller.list_capture_configs()
101
+ return {"status": "success", "configurations": configs}
102
+ except Exception as e:
103
+ return {"status": "error", "message": f"Failed to list capture configurations: {str(e)}"}
104
+
105
+ @mcp.tool("remove_device_config")
106
+ def remove_device_config(ctx: Context, name: str) -> Dict[str, Any]:
107
+ """Remove a device configuration."""
108
+ try:
109
+ if controller.remove_device_config(name):
110
+ return {"status": "success", "message": f"Removed device configuration: {name}"}
111
+ return {"status": "error", "message": f"Device configuration {name} not found"}
112
+ except Exception as e:
113
+ return {"status": "error", "message": f"Failed to remove device configuration: {str(e)}"}
114
+
115
+ @mcp.tool("remove_capture_config")
116
+ def remove_capture_config(ctx: Context, name: str) -> Dict[str, Any]:
117
+ """Remove a capture configuration."""
118
+ try:
119
+ if controller.remove_capture_config(name):
120
+ return {"status": "success", "message": f"Removed capture configuration: {name}"}
121
+ return {"status": "error", "message": f"Capture configuration {name} not found"}
122
+ except Exception as e:
123
+ return {"status": "error", "message": f"Failed to remove capture configuration: {str(e)}"}
124
+
125
+ @mcp.tool("get_digital_data")
126
+ def get_digital_data(ctx: Context,
127
+ capture_file: Optional[str] = None,
128
+ data: Optional[List[Dict[str, Union[float, bool]]]] = None,
129
+ channel: int = 0,
130
+ start_time: Optional[float] = None,
131
+ end_time: Optional[float] = None) -> Dict[str, Any]:
132
+ """
133
+ Get digital data for a specific channel.
134
+
135
+ Args:
136
+ capture_file: Path to the capture file (optional)
137
+ data: Direct data list of transitions (optional)
138
+ channel: Channel number
139
+ start_time: Start time in seconds (optional)
140
+ end_time: End time in seconds (optional)
141
+ """
142
+ try:
143
+ if data is not None:
144
+ # Filter data by time range if specified
145
+ if start_time is not None:
146
+ data = [d for d in data if d['time'] >= start_time]
147
+ if end_time is not None:
148
+ data = [d for d in data if d['time'] <= end_time]
149
+ return {
150
+ "status": "success",
151
+ "data": data
152
+ }
153
+ elif capture_file is not None:
154
+ if not os.path.exists(capture_file):
155
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
156
+
157
+ # Get Saleae instance
158
+ saleae_instance = get_saleae()
159
+ if saleae_instance is None:
160
+ return {
161
+ "status": "success",
162
+ "file_info": {
163
+ "path": capture_file,
164
+ "format": "Saleae Logic (.sal)",
165
+ "size": os.path.getsize(capture_file),
166
+ "modified": os.path.getmtime(capture_file)
167
+ }
168
+ }
169
+
170
+ try:
171
+ # Load the capture file using Saleae API
172
+ capture = saleae_instance.load_capture(capture_file)
173
+
174
+ # Get digital data for the specified channel
175
+ digital_data = capture.get_digital_data(channel, start_time, end_time)
176
+
177
+ # Convert to list of dictionaries
178
+ data = [
179
+ {
180
+ "time": point.time,
181
+ "value": point.value
182
+ }
183
+ for point in digital_data
184
+ ]
185
+
186
+ return {
187
+ "status": "success",
188
+ "channel": channel,
189
+ "data": data,
190
+ "total_samples": len(data)
191
+ }
192
+ except Exception as e:
193
+ return {
194
+ "status": "error",
195
+ "message": f"Failed to get digital data: {str(e)}"
196
+ }
197
+ else:
198
+ return {"status": "error", "message": "Either capture_file or data must be provided"}
199
+ except Exception as e:
200
+ return {"status": "error", "message": f"Failed to get digital data: {str(e)}"}
201
+
202
+ @mcp.tool("get_analog_data")
203
+ def get_analog_data(ctx: Context,
204
+ capture_file: Optional[str] = None,
205
+ data: Optional[List[Dict[str, Union[float, float]]]] = None,
206
+ channel: int = 0,
207
+ start_time: Optional[float] = None,
208
+ end_time: Optional[float] = None) -> Dict[str, Any]:
209
+ """
210
+ Get analog data for a specific channel.
211
+
212
+ Args:
213
+ capture_file: Path to the capture file (optional)
214
+ data: Direct data list of readings (optional)
215
+ channel: Channel number
216
+ start_time: Start time in seconds (optional)
217
+ end_time: End time in seconds (optional)
218
+ """
219
+ try:
220
+ if data is not None:
221
+ # Filter data by time range if specified
222
+ if start_time is not None:
223
+ data = [d for d in data if d['time'] >= start_time]
224
+ if end_time is not None:
225
+ data = [d for d in data if d['time'] <= end_time]
226
+ return {
227
+ "status": "success",
228
+ "data": data
229
+ }
230
+ elif capture_file is not None:
231
+ if not os.path.exists(capture_file):
232
+ raise FileNotFoundError(f"Capture file not found: {capture_file}")
233
+ saleae_instance = get_saleae()
234
+ if saleae_instance is None:
235
+ return {"status": "error", "message": "Saleae instance not available"}
236
+ data = saleae_instance.get_analog_data(capture_file, channel, start_time, end_time)
237
+ return {
238
+ "status": "success",
239
+ "data": data
240
+ }
241
+ else:
242
+ return {"status": "error", "message": "Either capture_file or data must be provided"}
243
+ except Exception as e:
244
+ return {"status": "error", "message": f"Failed to get analog data: {str(e)}"}
245
+
246
+ @mcp.tool("export_digital_data")
247
+ def export_digital_data(ctx: Context,
248
+ output_file: str,
249
+ capture_file: Optional[str] = None,
250
+ data: Optional[List[Dict[str, Union[float, bool]]]] = None,
251
+ channels: Optional[List[int]] = None,
252
+ start_time: Optional[float] = None,
253
+ end_time: Optional[float] = None) -> Dict[str, Any]:
254
+ """
255
+ Export digital data to a CSV file.
256
+
257
+ Args:
258
+ output_file: Path to output CSV file
259
+ capture_file: Path to the capture file (optional)
260
+ data: Direct data list of transitions (optional)
261
+ channels: List of channels to export (optional)
262
+ start_time: Start time in seconds (optional)
263
+ end_time: End time in seconds (optional)
264
+ """
265
+ try:
266
+ if data is not None:
267
+ # Filter data by time range if specified
268
+ if start_time is not None:
269
+ data = [d for d in data if d['time'] >= start_time]
270
+ if end_time is not None:
271
+ data = [d for d in data if d['time'] <= end_time]
272
+ # Export to CSV
273
+ with open(output_file, 'w') as f:
274
+ f.write("Time,Value\n")
275
+ for entry in data:
276
+ f.write(f"{entry['time']},{entry['value']}\n")
277
+ return {
278
+ "status": "success",
279
+ "message": f"Exported digital data to {output_file}"
280
+ }
281
+ elif capture_file is not None:
282
+ if not os.path.exists(capture_file):
283
+ raise FileNotFoundError(f"Capture file not found: {capture_file}")
284
+ saleae_instance = get_saleae()
285
+ if saleae_instance is None:
286
+ return {"status": "error", "message": "Saleae instance not available"}
287
+ saleae_instance.export_digital_data(capture_file, output_file, channels, start_time, end_time)
288
+ return {
289
+ "status": "success",
290
+ "message": f"Exported digital data to {output_file}"
291
+ }
292
+ else:
293
+ return {"status": "error", "message": "Either capture_file or data must be provided"}
294
+ except Exception as e:
295
+ return {"status": "error", "message": f"Failed to export digital data: {str(e)}"}
296
+
297
+ @mcp.tool("export_analog_data")
298
+ def export_analog_data(ctx: Context,
299
+ output_file: str,
300
+ capture_file: Optional[str] = None,
301
+ data: Optional[List[Dict[str, Union[float, float]]]] = None,
302
+ channels: Optional[List[int]] = None,
303
+ start_time: Optional[float] = None,
304
+ end_time: Optional[float] = None) -> Dict[str, Any]:
305
+ """
306
+ Export analog data to a CSV file.
307
+
308
+ Args:
309
+ output_file: Path to output CSV file
310
+ capture_file: Path to the capture file (optional)
311
+ data: Direct data list of readings (optional)
312
+ channels: List of channels to export (optional)
313
+ start_time: Start time in seconds (optional)
314
+ end_time: End time in seconds (optional)
315
+ """
316
+ try:
317
+ if data is not None:
318
+ # Filter data by time range if specified
319
+ if start_time is not None:
320
+ data = [d for d in data if d['time'] >= start_time]
321
+ if end_time is not None:
322
+ data = [d for d in data if d['time'] <= end_time]
323
+ # Export to CSV
324
+ with open(output_file, 'w') as f:
325
+ f.write("Time,Voltage\n")
326
+ for entry in data:
327
+ f.write(f"{entry['time']},{entry['voltage']}\n")
328
+ return {
329
+ "status": "success",
330
+ "message": f"Exported analog data to {output_file}"
331
+ }
332
+ elif capture_file is not None:
333
+ if not os.path.exists(capture_file):
334
+ raise FileNotFoundError(f"Capture file not found: {capture_file}")
335
+ saleae_instance = get_saleae()
336
+ if saleae_instance is None:
337
+ return {"status": "error", "message": "Saleae instance not available"}
338
+ saleae_instance.export_analog_data(capture_file, output_file, channels, start_time, end_time)
339
+ return {
340
+ "status": "success",
341
+ "message": f"Exported analog data to {output_file}"
342
+ }
343
+ else:
344
+ return {"status": "error", "message": "Either capture_file or data must be provided"}
345
+ except Exception as e:
346
+ return {"status": "error", "message": f"Failed to export analog data: {str(e)}"}
347
+
348
+
349
+ # Protocol and Data Analysis tools
350
+ @mcp.tool("detect_protocols")
351
+ def detect_protocols(ctx: Context, capture_file: str) -> Dict[str, Any]:
352
+ """
353
+ Detect protocols in a capture file.
354
+
355
+ Args:
356
+ capture_file: Path to the capture file
357
+ """
358
+ try:
359
+ if not os.path.exists(capture_file):
360
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
361
+
362
+ saleae_instance = get_saleae()
363
+ if saleae_instance is None:
364
+ return {
365
+ "status": "success",
366
+ "file_info": {
367
+ "path": capture_file,
368
+ "format": "Saleae Logic (.sal)",
369
+ "size": os.path.getsize(capture_file),
370
+ "modified": os.path.getmtime(capture_file)
371
+ }
372
+ }
373
+
374
+ # Load capture and get protocol analyzers
375
+ capture = saleae_instance.load_capture(capture_file)
376
+ analyzers = capture.get_analyzers()
377
+
378
+ return {
379
+ "status": "success",
380
+ "protocols": [
381
+ {
382
+ "name": analyzer.name,
383
+ "type": analyzer.type,
384
+ "channels": analyzer.channels,
385
+ "settings": analyzer.settings
386
+ }
387
+ for analyzer in analyzers
388
+ ]
389
+ }
390
+ except Exception as e:
391
+ return {"status": "error", "message": f"Failed to detect protocols: {str(e)}"}
392
+
393
+ @mcp.tool("get_protocol_data")
394
+ def get_protocol_data(ctx: Context,
395
+ capture_file: str,
396
+ protocol_type: str,
397
+ start_time: Optional[float] = None,
398
+ end_time: Optional[float] = None) -> Dict[str, Any]:
399
+ """
400
+ Get protocol data from a capture file.
401
+
402
+ Args:
403
+ capture_file: Path to the capture file
404
+ protocol_type: Type of protocol to analyze (e.g., 'I2C', 'SPI', 'UART')
405
+ start_time: Start time in seconds (optional)
406
+ end_time: End time in seconds (optional)
407
+ """
408
+ try:
409
+ if not os.path.exists(capture_file):
410
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
411
+
412
+ saleae_instance = get_saleae()
413
+ if saleae_instance is None:
414
+ return {
415
+ "status": "success",
416
+ "file_info": {
417
+ "path": capture_file,
418
+ "format": "Saleae Logic (.sal)",
419
+ "size": os.path.getsize(capture_file),
420
+ "modified": os.path.getmtime(capture_file)
421
+ }
422
+ }
423
+
424
+ # Load capture and get protocol analyzer
425
+ capture = saleae_instance.load_capture(capture_file)
426
+ analyzer = capture.get_analyzer(protocol_type)
427
+
428
+ if analyzer is None:
429
+ return {"status": "error", "message": f"No {protocol_type} analyzer found"}
430
+
431
+ # Get protocol data
432
+ data = analyzer.get_data(start_time, end_time)
433
+
434
+ return {
435
+ "status": "success",
436
+ "protocol": protocol_type,
437
+ "data": [
438
+ {
439
+ "time": packet.time,
440
+ "type": packet.type,
441
+ "data": packet.data,
442
+ "metadata": packet.metadata
443
+ }
444
+ for packet in data
445
+ ]
446
+ }
447
+ except Exception as e:
448
+ return {"status": "error", "message": f"Failed to get protocol data: {str(e)}"}
449
+
450
+ @mcp.tool("get_digital_data_batch_mcp")
451
+ def get_digital_data_batch_mcp(ctx: Context,
452
+ capture_file: str,
453
+ channels: List[int],
454
+ start_time: Optional[float] = None,
455
+ end_time: Optional[float] = None,
456
+ max_samples: Optional[int] = None) -> Dict[str, Any]:
457
+ """Get digital data from multiple channels in a capture file."""
458
+ try:
459
+ from controllers.saleae_controller import SaleaeController
460
+ controller = SaleaeController()
461
+ return controller.get_digital_data_batch(
462
+ capture_file=capture_file,
463
+ channels=channels,
464
+ start_time=start_time,
465
+ end_time=end_time,
466
+ max_samples=max_samples
467
+ )
468
+ except Exception as e:
469
+ return {"status": "error", "message": f"Error getting digital data: {str(e)}"}
470
+
471
+
472
+ # TODO: Temporarily disabled analyze functions - will be re-enabled after Saleae API integration is complete
473
+ """
474
+ @mcp.tool("analyze_digital_data")
475
+ def analyze_digital_data(ctx: Context,
476
+ capture_file: str,
477
+ channel: int,
478
+ start_time: Optional[float] = None,
479
+ end_time: Optional[float] = None) -> Dict[str, Any]:
480
+ Analyze digital data from a capture file.
481
+
482
+ Args:
483
+ capture_file: Path to the capture file
484
+ channel: Channel number to analyze
485
+ start_time: Start time in seconds (optional)
486
+ end_time: End time in seconds (optional)
487
+ try:
488
+ if not os.path.exists(capture_file):
489
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
490
+
491
+ saleae_instance = get_saleae()
492
+ if saleae_instance is None:
493
+ return {
494
+ "status": "success",
495
+ "file_info": {
496
+ "path": capture_file,
497
+ "format": "Saleae Logic (.sal)",
498
+ "size": os.path.getsize(capture_file),
499
+ "modified": os.path.getmtime(capture_file)
500
+ }
501
+ }
502
+
503
+ # Load capture and get digital data
504
+ capture = saleae_instance.load_capture(capture_file)
505
+ data = capture.get_digital_data(channel, start_time, end_time)
506
+
507
+ # Analyze transitions
508
+ transitions = []
509
+ for i in range(len(data) - 1):
510
+ if data[i].value != data[i + 1].value:
511
+ transitions.append({
512
+ "time": data[i + 1].time,
513
+ "from_value": data[i].value,
514
+ "to_value": data[i + 1].value
515
+ })
516
+
517
+ return {
518
+ "status": "success",
519
+ "channel": channel,
520
+ "total_samples": len(data),
521
+ "transitions": transitions,
522
+ "first_value": data[0].value if data else None,
523
+ "last_value": data[-1].value if data else None
524
+ }
525
+ except Exception as e:
526
+ return {"status": "error", "message": f"Failed to analyze digital data: {str(e)}"}
527
+
528
+ @mcp.tool("analyze_analog_data")
529
+ def analyze_analog_data(ctx: Context,
530
+ capture_file: str,
531
+ channel: int,
532
+ start_time: Optional[float] = None,
533
+ end_time: Optional[float] = None) -> Dict[str, Any]:
534
+ Analyze analog data from a capture file.
535
+
536
+ Args:
537
+ capture_file: Path to the capture file
538
+ channel: Channel number to analyze
539
+ start_time: Start time in seconds (optional)
540
+ end_time: End time in seconds (optional)
541
+ try:
542
+ if not os.path.exists(capture_file):
543
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
544
+
545
+ saleae_instance = get_saleae()
546
+ if saleae_instance is None:
547
+ return {
548
+ "status": "success",
549
+ "file_info": {
550
+ "path": capture_file,
551
+ "format": "Saleae Logic (.sal)",
552
+ "size": os.path.getsize(capture_file),
553
+ "modified": os.path.getmtime(capture_file)
554
+ }
555
+ }
556
+
557
+ # Load capture and get analog data
558
+ capture = saleae_instance.load_capture(capture_file)
559
+ data = capture.get_analog_data(channel, start_time, end_time)
560
+
561
+ if not data:
562
+ return {"status": "error", "message": "No analog data found"}
563
+
564
+ # Calculate statistics
565
+ values = [point.voltage for point in data]
566
+ min_voltage = min(values)
567
+ max_voltage = max(values)
568
+ avg_voltage = sum(values) / len(values)
569
+
570
+ return {
571
+ "status": "success",
572
+ "channel": channel,
573
+ "total_samples": len(data),
574
+ "min_voltage": min_voltage,
575
+ "max_voltage": max_voltage,
576
+ "avg_voltage": avg_voltage,
577
+ "first_value": data[0].voltage if data else None,
578
+ "last_value": data[-1].voltage if data else None
579
+ }
580
+ except Exception as e:
581
+ return {"status": "error", "message": f"Failed to analyze analog data: {str(e)}"}
582
+
583
+ @mcp.tool("export_protocol_data")
584
+ def export_protocol_data(ctx: Context,
585
+ output_file: str,
586
+ capture_file: str,
587
+ protocol_type: str,
588
+ start_time: Optional[float] = None,
589
+ end_time: Optional[float] = None) -> Dict[str, Any]:
590
+ Export protocol data to a CSV file.
591
+
592
+ Args:
593
+ output_file: Path to output CSV file
594
+ capture_file: Path to the capture file
595
+ protocol_type: Type of protocol to analyze
596
+ start_time: Start time in seconds (optional)
597
+ end_time: End time in seconds (optional)
598
+ try:
599
+ if not os.path.exists(capture_file):
600
+ return {"status": "error", "message": f"Capture file not found: {capture_file}"}
601
+
602
+ saleae_instance = get_saleae()
603
+ if saleae_instance is None:
604
+ return {
605
+ "status": "success",
606
+ "file_info": {
607
+ "path": capture_file,
608
+ "format": "Saleae Logic (.sal)",
609
+ "size": os.path.getsize(capture_file),
610
+ "modified": os.path.getmtime(capture_file)
611
+ }
612
+ }
613
+
614
+ # Load capture and get protocol analyzer
615
+ capture = saleae_instance.load_capture(capture_file)
616
+ analyzer = capture.get_analyzer(protocol_type)
617
+
618
+ if analyzer is None:
619
+ return {"status": "error", "message": f"No {protocol_type} analyzer found"}
620
+
621
+ # Get protocol data
622
+ data = analyzer.get_data(start_time, end_time)
623
+
624
+ # Export to CSV
625
+ with open(output_file, 'w') as f:
626
+ f.write("Time,Type,Data,Metadata\n")
627
+ for packet in data:
628
+ f.write(f"{packet.time},{packet.type},{packet.data},{packet.metadata}\n")
629
+
630
+ return {
631
+ "status": "success",
632
+ "message": f"Exported {protocol_type} data to {output_file}"
633
+ }
634
+ except Exception as e:
635
+ return {"status": "error", "message": f"Failed to export protocol data: {str(e)}"}
636
+ """