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,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
|
+
"""
|