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,310 @@
|
|
|
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
|
+
import sys # added for argv inspection
|
|
11
|
+
from logic_analyzer_mcp.mcp_tools_experimental import setup_mcp_tools_experimental
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Use shared saleae manager for instance creation/caching
|
|
16
|
+
from logic_analyzer_mcp.saleae_manager import get_saleae
|
|
17
|
+
|
|
18
|
+
def setup_mcp_tools(mcp: FastMCP, controller=None, enable_logic2: Optional[bool] = None) -> None:
|
|
19
|
+
"""Setup MCP tools for Saleae Logic control.
|
|
20
|
+
|
|
21
|
+
If enable_logic2 is None, detection falls back to environment LOGIC2 or CLI args (--logic2).
|
|
22
|
+
"""
|
|
23
|
+
# Determine whether to enable Logic2 experimental tools
|
|
24
|
+
if enable_logic2 is None:
|
|
25
|
+
env_val = os.environ.get("LOGIC2")
|
|
26
|
+
if env_val and str(env_val).lower() in ("1", "true", "yes"):
|
|
27
|
+
use_logic2 = True
|
|
28
|
+
elif any(arg == "logic2" or arg.startswith("--logic2") for arg in sys.argv[1:]):
|
|
29
|
+
use_logic2 = True
|
|
30
|
+
else:
|
|
31
|
+
use_logic2 = False
|
|
32
|
+
else:
|
|
33
|
+
use_logic2 = bool(enable_logic2)
|
|
34
|
+
|
|
35
|
+
if use_logic2:
|
|
36
|
+
try:
|
|
37
|
+
setup_mcp_tools_experimental(mcp, controller)
|
|
38
|
+
logger.info("Logic2 experimental MCP tools enabled (setup_mcp_tools_experimental called).")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.warning(f"setup_mcp_tools_experimental not available or failed: {e}")
|
|
41
|
+
else:
|
|
42
|
+
logger.info("Logic2 experimental MCP tools not enabled.")
|
|
43
|
+
|
|
44
|
+
# Add python-saleae specific tools
|
|
45
|
+
@mcp.tool("saleae_connect")
|
|
46
|
+
def saleae_connect(ctx: Context) -> Dict[str, Any]:
|
|
47
|
+
"""Connect to Saleae Logic software using python-saleae."""
|
|
48
|
+
try:
|
|
49
|
+
from controllers.saleae_controller import SaleaeController
|
|
50
|
+
controller = SaleaeController()
|
|
51
|
+
if controller.connect():
|
|
52
|
+
return {"status": "success", "message": "Connected to Saleae Logic"}
|
|
53
|
+
return {"status": "error", "message": "Failed to connect to Saleae Logic"}
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return {"status": "error", "message": f"Error connecting to Saleae Logic: {str(e)}"}
|
|
56
|
+
|
|
57
|
+
@mcp.tool("saleae_configure")
|
|
58
|
+
def saleae_configure(ctx: Context,
|
|
59
|
+
digital_channels: List[int],
|
|
60
|
+
digital_sample_rate: int,
|
|
61
|
+
analog_channels: Optional[List[int]] = None,
|
|
62
|
+
analog_sample_rate: Optional[int] = None,
|
|
63
|
+
trigger_channel: Optional[int] = None,
|
|
64
|
+
trigger_type: Optional[str] = None) -> Dict[str, Any]:
|
|
65
|
+
"""Configure Saleae Logic capture settings."""
|
|
66
|
+
try:
|
|
67
|
+
from controllers.saleae_controller import SaleaeController
|
|
68
|
+
controller = SaleaeController()
|
|
69
|
+
if controller.connect():
|
|
70
|
+
if controller.configure_capture(
|
|
71
|
+
digital_channels=digital_channels,
|
|
72
|
+
digital_sample_rate=digital_sample_rate,
|
|
73
|
+
analog_channels=analog_channels,
|
|
74
|
+
analog_sample_rate=analog_sample_rate,
|
|
75
|
+
trigger_channel=trigger_channel,
|
|
76
|
+
trigger_type=trigger_type
|
|
77
|
+
):
|
|
78
|
+
return {"status": "success", "message": "Configured Saleae Logic capture"}
|
|
79
|
+
return {"status": "error", "message": "Failed to configure capture"}
|
|
80
|
+
return {"status": "error", "message": "Failed to connect to Saleae Logic"}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return {"status": "error", "message": f"Error configuring Saleae Logic: {str(e)}"}
|
|
83
|
+
|
|
84
|
+
@mcp.tool("saleae_capture")
|
|
85
|
+
def saleae_capture(ctx: Context,
|
|
86
|
+
duration_seconds: float,
|
|
87
|
+
output_file: str) -> Dict[str, Any]:
|
|
88
|
+
"""Start a capture with Saleae Logic and save to file."""
|
|
89
|
+
try:
|
|
90
|
+
from controllers.saleae_controller import SaleaeController
|
|
91
|
+
controller = SaleaeController()
|
|
92
|
+
if controller.connect():
|
|
93
|
+
if controller.start_capture(duration_seconds):
|
|
94
|
+
# Wait for capture to complete
|
|
95
|
+
time.sleep(duration_seconds + 1) # Add 1 second buffer
|
|
96
|
+
if controller.save_capture(output_file):
|
|
97
|
+
return {"status": "success", "message": f"Capture saved to {output_file}"}
|
|
98
|
+
return {"status": "error", "message": "Failed to save capture"}
|
|
99
|
+
return {"status": "error", "message": "Failed to start capture"}
|
|
100
|
+
return {"status": "error", "message": "Failed to connect to Saleae Logic"}
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return {"status": "error", "message": f"Error during capture: {str(e)}"}
|
|
103
|
+
|
|
104
|
+
@mcp.tool("saleae_export")
|
|
105
|
+
def saleae_export(ctx: Context,
|
|
106
|
+
input_file: str,
|
|
107
|
+
output_file: str,
|
|
108
|
+
format: str = 'csv',
|
|
109
|
+
digital_channels: Optional[List[int]] = None,
|
|
110
|
+
analog_channels: Optional[List[int]] = None,
|
|
111
|
+
time_span: Optional[List[float]] = None) -> Dict[str, Any]:
|
|
112
|
+
"""Export capture data to specified format."""
|
|
113
|
+
try:
|
|
114
|
+
from controllers.saleae_controller import SaleaeController
|
|
115
|
+
controller = SaleaeController()
|
|
116
|
+
return controller.export_data(
|
|
117
|
+
input_file=input_file,
|
|
118
|
+
output_file=output_file,
|
|
119
|
+
format=format,
|
|
120
|
+
digital_channels=digital_channels,
|
|
121
|
+
analog_channels=analog_channels,
|
|
122
|
+
time_span=time_span
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return {"status": "error", "message": f"Error during export: {str(e)}"}
|
|
126
|
+
|
|
127
|
+
@mcp.tool("saleae_device_info")
|
|
128
|
+
def saleae_device_info(ctx: Context) -> Dict[str, Any]:
|
|
129
|
+
"""Get information about the connected Saleae Logic device."""
|
|
130
|
+
try:
|
|
131
|
+
from controllers.saleae_controller import SaleaeController
|
|
132
|
+
controller = SaleaeController()
|
|
133
|
+
if controller.connect():
|
|
134
|
+
info = controller.get_device_info()
|
|
135
|
+
if info:
|
|
136
|
+
return {"status": "success", "device_info": info}
|
|
137
|
+
return {"status": "error", "message": "Failed to get device info"}
|
|
138
|
+
return {"status": "error", "message": "Failed to connect to Saleae Logic"}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {"status": "error", "message": f"Error getting device info: {str(e)}"}
|
|
141
|
+
|
|
142
|
+
# Parser-related tools
|
|
143
|
+
@mcp.tool("parse_capture_file")
|
|
144
|
+
def parse_capture_file(ctx: Context,
|
|
145
|
+
capture_file: Optional[str] = None,
|
|
146
|
+
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Initialize parser with a capture file or direct data and return basic information.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
capture_file: Path to the capture file (optional)
|
|
152
|
+
data: Direct data dictionary with duration, digital_channels, and analog_channels (optional)
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
if data is not None:
|
|
156
|
+
# Validate required fields
|
|
157
|
+
required_fields = ['duration', 'digital_channels', 'analog_channels']
|
|
158
|
+
if not all(field in data for field in required_fields):
|
|
159
|
+
return {"status": "error", "message": f"Missing required fields: {required_fields}"}
|
|
160
|
+
return {
|
|
161
|
+
"status": "success",
|
|
162
|
+
"duration": data['duration'],
|
|
163
|
+
"digital_channels": data['digital_channels'],
|
|
164
|
+
"analog_channels": data['analog_channels']
|
|
165
|
+
}
|
|
166
|
+
elif capture_file is not None:
|
|
167
|
+
if not os.path.exists(capture_file):
|
|
168
|
+
return {"status": "error", "message": f"Capture file not found: {capture_file}"}
|
|
169
|
+
|
|
170
|
+
# Get Saleae instance
|
|
171
|
+
saleae_instance = get_saleae()
|
|
172
|
+
if saleae_instance is None:
|
|
173
|
+
# Try to launch Saleae software
|
|
174
|
+
try:
|
|
175
|
+
# First check if Saleae is installed in common locations
|
|
176
|
+
common_paths = [
|
|
177
|
+
os.path.expandvars(r"%ProgramFiles%\\Saleae\\Logic\\Logic.exe"),
|
|
178
|
+
os.path.expandvars(r"%ProgramFiles(x86)%\\Saleae\\Logic\\Logic.exe"),
|
|
179
|
+
os.path.expanduser("~\\AppData\\Local\\Programs\\Saleae\\Logic\\Logic.exe")
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
saleae_path = None
|
|
183
|
+
for path in common_paths:
|
|
184
|
+
if os.path.exists(path):
|
|
185
|
+
saleae_path = path
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
if saleae_path is None:
|
|
189
|
+
# Fall back to offline mode
|
|
190
|
+
logger.warning("Saleae Logic software not found. Falling back to offline mode.")
|
|
191
|
+
return {
|
|
192
|
+
"status": "success",
|
|
193
|
+
# "message": "Running in offline mode",
|
|
194
|
+
"file_info": {
|
|
195
|
+
"path": capture_file,
|
|
196
|
+
"format": "Saleae Logic (.sal)",
|
|
197
|
+
"size": os.path.getsize(capture_file),
|
|
198
|
+
"modified": os.path.getmtime(capture_file)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Try to launch with the found path
|
|
203
|
+
saleae_instance = Saleae()
|
|
204
|
+
saleae_instance.launch()
|
|
205
|
+
# Wait a bit for the software to start
|
|
206
|
+
time.sleep(2)
|
|
207
|
+
except Exception as launch_error:
|
|
208
|
+
# Fall back to offline mode
|
|
209
|
+
logger.warning(f"Failed to launch Saleae software: {launch_error}. Falling back to offline mode.")
|
|
210
|
+
return {
|
|
211
|
+
"status": "success",
|
|
212
|
+
# "message": "Running in offline mode",
|
|
213
|
+
"file_info": {
|
|
214
|
+
"path": capture_file,
|
|
215
|
+
"format": "Saleae Logic (.sal)",
|
|
216
|
+
"size": os.path.getsize(capture_file),
|
|
217
|
+
"modified": os.path.getmtime(capture_file)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# Load the capture file using Saleae API
|
|
223
|
+
capture = saleae_instance.load_capture(capture_file)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"status": "success",
|
|
227
|
+
"file_info": {
|
|
228
|
+
"path": capture_file,
|
|
229
|
+
"format": "Saleae Logic (.sal)",
|
|
230
|
+
"duration": capture.duration,
|
|
231
|
+
"digital_channels": capture.digital_channels,
|
|
232
|
+
"analog_channels": capture.analog_channels,
|
|
233
|
+
"digital_sample_rate": capture.digital_sample_rate,
|
|
234
|
+
"analog_sample_rate": capture.analog_sample_rate
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
except Exception as e:
|
|
238
|
+
# Fall back to offline mode
|
|
239
|
+
logger.warning(f"Failed to parse capture file with Saleae API: {e}. Falling back to offline mode.")
|
|
240
|
+
return {
|
|
241
|
+
"status": "success",
|
|
242
|
+
# "message": "Running in offline mode",
|
|
243
|
+
"file_info": {
|
|
244
|
+
"path": capture_file,
|
|
245
|
+
"format": "Saleae Logic (.sal)",
|
|
246
|
+
"size": os.path.getsize(capture_file),
|
|
247
|
+
"modified": os.path.getmtime(capture_file)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else:
|
|
251
|
+
return {"status": "error", "message": "Either capture_file or data must be provided"}
|
|
252
|
+
except Exception as e:
|
|
253
|
+
return {"status": "error", "message": f"Failed to parse capture file: {str(e)}"}
|
|
254
|
+
|
|
255
|
+
@mcp.tool("get_sample_rate")
|
|
256
|
+
def get_sample_rate(ctx: Context,
|
|
257
|
+
capture_file: Optional[str] = None,
|
|
258
|
+
sample_rate: Optional[float] = None,
|
|
259
|
+
channel: int = 0) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Get the sample rate for a specific channel.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
capture_file: Path to the capture file (optional)
|
|
265
|
+
sample_rate: Direct sample rate value (optional)
|
|
266
|
+
channel: Channel number
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
if sample_rate is not None:
|
|
270
|
+
return {
|
|
271
|
+
"status": "success",
|
|
272
|
+
"sample_rate": sample_rate
|
|
273
|
+
}
|
|
274
|
+
elif capture_file is not None:
|
|
275
|
+
if not os.path.exists(capture_file):
|
|
276
|
+
raise FileNotFoundError(f"Capture file not found: {capture_file}")
|
|
277
|
+
saleae_instance = get_saleae()
|
|
278
|
+
if saleae_instance is None:
|
|
279
|
+
return {"status": "error", "message": "Saleae instance not available"}
|
|
280
|
+
rate = saleae_instance.get_sample_rate(capture_file, channel)
|
|
281
|
+
return {
|
|
282
|
+
"status": "success",
|
|
283
|
+
"sample_rate": rate
|
|
284
|
+
}
|
|
285
|
+
else:
|
|
286
|
+
return {"status": "error", "message": "Either capture_file or sample_rate must be provided"}
|
|
287
|
+
except Exception as e:
|
|
288
|
+
return {"status": "error", "message": f"Failed to get sample rate: {str(e)}"}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@mcp.tool("get_digital_data_mcp")
|
|
292
|
+
def get_digital_data_mcp(ctx: Context,
|
|
293
|
+
capture_file: str,
|
|
294
|
+
channel: int = 0,
|
|
295
|
+
start_time: Optional[float] = None,
|
|
296
|
+
end_time: Optional[float] = None,
|
|
297
|
+
max_samples: Optional[int] = None) -> Dict[str, Any]:
|
|
298
|
+
"""Get digital data from a capture file."""
|
|
299
|
+
try:
|
|
300
|
+
from controllers.saleae_controller import SaleaeController
|
|
301
|
+
controller = SaleaeController()
|
|
302
|
+
return controller.get_digital_data(
|
|
303
|
+
capture_file=capture_file,
|
|
304
|
+
channel=channel,
|
|
305
|
+
start_time=start_time,
|
|
306
|
+
end_time=end_time,
|
|
307
|
+
max_samples=max_samples
|
|
308
|
+
)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return {"status": "error", "message": f"Error getting digital data: {str(e)}"}
|