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,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()
|