wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ConnectionHandler.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <memory>
7
8#include <QJsonArray>
9
12#include "App/Scene/Commands.h"
14#include "App/Scene/Scene.h"
16#include "App/Wiring/Port.h"
17
19 : BaseHandler(mainWindow, validator)
20{
21}
22
23QJsonObject ConnectionHandler::handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId)
24{
25 if (command == "connect_elements") {
26 return handleConnectElements(params, requestId);
27 } else if (command == "disconnect_elements") {
28 return handleDisconnectElements(params, requestId);
29 } else if (command == "list_connections") {
30 return handleListConnections(params, requestId);
31 } else if (command == "split_connection") {
32 return handleSplitConnection(params, requestId);
33 } else {
34 return createErrorResponse(QString("Unknown connection command: %1").arg(command),
36 }
37}
38
39QJsonObject ConnectionHandler::handleConnectElements(const QJsonObject &params, const QJsonValue &requestId)
40{
41 // Required: source_id, target_id
42 // For ports: either use index (source_port, target_port) or label (source_port_label, target_port_label)
43 if (!validateParameters(params, {"source_id", "target_id"})) {
44 return createErrorResponse("Missing required parameters: source_id, target_id", requestId, JsonRpcError::InvalidParams);
45 }
46
47 QString errorMsg;
48 auto *sourceElement = validatedElement(params, "source_id", errorMsg);
49 if (!sourceElement) {
50 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
51 }
52 auto *targetElement = validatedElement(params, "target_id", errorMsg);
53 if (!targetElement) {
54 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
55 }
56
57 int sourcePort = -1;
58 if (!resolvePort(params, "source", sourceElement, true, sourcePort, errorMsg)) {
59 return createErrorResponse(errorMsg, requestId, JsonRpcError::PortNotFound);
60 }
61
62 int targetPort = -1;
63 if (!resolvePort(params, "target", targetElement, false, targetPort, errorMsg)) {
64 return createErrorResponse(errorMsg, requestId, JsonRpcError::PortNotFound);
65 }
66
67 Port *outputPort = sourceElement->outputPort(sourcePort);
68 Port *inputPort = targetElement->inputPort(targetPort);
69
70 if (!outputPort || !inputPort) {
71 return createErrorResponse("Invalid port specification", requestId, JsonRpcError::PortNotFound);
72 }
73
74 auto *startPort = dynamic_cast<OutputPort *>(outputPort);
75 auto *endPort = dynamic_cast<InputPort *>(inputPort);
76
77 // Enforce the same rules the UI applies on wire drop (F21): no duplicate
78 // connections, no second driver on an occupied input, no wires onto
79 // wireless Tx/Rx ports. Without this, MCP could build circuits the UI
80 // forbids (the simulator degrades them to Error status).
81 if (!ConnectionManager::isConnectionAllowed(startPort, endPort)) {
82 return createErrorResponse(QString("Connection from element %1 port %2 to element %3 port %4 is not allowed "
83 "(duplicate, occupied input, or wireless port)")
84 .arg(sourceElement->id()).arg(sourcePort).arg(targetElement->id()).arg(targetPort),
86 }
87
88 Scene *scene = currentScene();
89 if (!scene) {
90 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
91 }
92
93 auto connection = std::make_unique<Connection>();
94 connection->setStartPort(startPort);
95 connection->setEndPort(endPort);
96 connection->updatePath();
97
98 try {
99 scene->receiveCommand(new AddItemsCommand({connection.get()}, scene));
100 connection.release(); // scene/command takes ownership
101 } catch (const std::exception &e) {
102 return createErrorResponse(QString("Failed to connect elements: %1").arg(e.what()),
104 } catch (...) {
105 return createErrorResponse("Failed to connect elements: Unknown exception",
107 }
108
109 return createSuccessResponse(QJsonObject(), requestId);
110}
111
112QJsonObject ConnectionHandler::handleDisconnectElements(const QJsonObject &params, const QJsonValue &requestId)
113{
114 if (!validateParameters(params, {"source_id", "target_id"})) {
115 return createErrorResponse("Missing required parameters: source_id, target_id", requestId, JsonRpcError::InvalidParams);
116 }
117
118 QString errorMsg;
119 auto *sourceElement = validatedElement(params, "source_id", errorMsg);
120 if (!sourceElement) {
121 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
122 }
123 auto *targetElement = validatedElement(params, "target_id", errorMsg);
124 if (!targetElement) {
125 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
126 }
127
128 Scene *scene = currentScene();
129 if (!scene) {
130 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
131 }
132
133 const auto connections = scene->items();
134 for (auto *item : connections) {
135 auto *connection = qgraphicsitem_cast<Connection *>(item);
136 if (!connection) {
137 continue;
138 }
139
140 Port *port1 = connection->startPort();
141 Port *port2 = connection->endPort();
142
143 if (!port1 || !port2) {
144 continue;
145 }
146
147 GraphicElement *elem1 = port1->graphicElement();
148 GraphicElement *elem2 = port2->graphicElement();
149
150 if ((elem1 == sourceElement && elem2 == targetElement) ||
151 (elem1 == targetElement && elem2 == sourceElement)) {
152 return tryCommand([&] {
153 scene->receiveCommand(new DeleteItemsCommand({connection}, scene));
154 return createSuccessResponse(QJsonObject(), requestId);
155 }, "disconnect elements", requestId);
156 }
157 }
158
159 return createErrorResponse(QString("No connection found between elements %1 and %2").arg(sourceElement->id()).arg(targetElement->id()),
161}
162
163QJsonObject ConnectionHandler::handleListConnections(const QJsonObject &, const QJsonValue &requestId)
164{
165 Scene *scene = currentScene();
166 if (!scene) {
167 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
168 }
169
170 QJsonArray connections;
171 const auto sceneItems = scene->items();
172
173 for (const auto *item : sceneItems) {
174 const auto *connection = qgraphicsitem_cast<const Connection *>(item);
175 if (!connection) {
176 continue;
177 }
178
179 const Port *startPort = connection->startPort();
180 const Port *endPort = connection->endPort();
181
182 if (!startPort || !endPort) {
183 continue;
184 }
185
186 const GraphicElement *startElement = startPort->graphicElement();
187 const GraphicElement *endElement = endPort->graphicElement();
188
189 if (!startElement || !endElement) {
190 continue;
191 }
192
193 QJsonObject connectionObj;
194 connectionObj["source_id"] = startElement->id();
195 connectionObj["source_port"] = startPort->index();
196 connectionObj["target_id"] = endElement->id();
197 connectionObj["target_port"] = endPort->index();
198
199 connections.append(connectionObj);
200 }
201
202 QJsonObject result;
203 result["connections"] = connections;
204
205 return createSuccessResponse(result, requestId);
206}
207
208QJsonObject ConnectionHandler::handleSplitConnection(const QJsonObject &params, const QJsonValue &requestId)
209{
210 if (!validateParameters(params, {"source_id", "source_port", "target_id", "target_port", "x", "y"})) {
211 return createErrorResponse("Missing required parameters: source_id, source_port, target_id, target_port, x, y",
212 requestId, JsonRpcError::InvalidParams);
213 }
214
215 QString errorMsg;
216
217 if (!validateNonNegativeInteger(params.value("source_port"), "source_port", errorMsg)) {
218 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
219 }
220 if (!validateNonNegativeInteger(params.value("target_port"), "target_port", errorMsg)) {
221 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
222 }
223 if (!validateNumeric(params.value("x"), "x", errorMsg)) {
224 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
225 }
226 if (!validateNumeric(params.value("y"), "y", errorMsg)) {
227 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
228 }
229
230 const int sourcePort = params.value("source_port").toInt();
231 const int targetPort = params.value("target_port").toInt();
232 const double x = params.value("x").toDouble();
233 const double y = params.value("y").toDouble();
234
235 auto *sourceElement = validatedElement(params, "source_id", errorMsg);
236 if (!sourceElement) {
237 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
238 }
239 auto *targetElement = validatedElement(params, "target_id", errorMsg);
240 if (!targetElement) {
241 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
242 }
243
244 Scene *scene = currentScene();
245 if (!scene) {
246 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
247 }
248
249 // Find the connection between source and target
250 Connection *connectionToSplit = nullptr;
251 const auto sceneItems = scene->items();
252
253 for (auto *item : sceneItems) {
254 auto *connection = qgraphicsitem_cast<Connection *>(item);
255 if (!connection) { continue; }
256
257 Port *port1 = connection->startPort();
258 Port *port2 = connection->endPort();
259
260 if (!port1 || !port2) {
261 continue;
262 }
263
264 GraphicElement *elem1 = port1->graphicElement();
265 GraphicElement *elem2 = port2->graphicElement();
266
267 if (!elem1 || !elem2) {
268 continue;
269 }
270
271 // Check if this connection matches source->target
272 if (elem1 == sourceElement && elem2 == targetElement &&
273 port1->index() == sourcePort && port2->index() == targetPort) {
274 connectionToSplit = connection;
275 break;
276 }
277 }
278
279 if (!connectionToSplit) {
280 return createErrorResponse("Connection not found between specified source and target",
282 }
283
284 return tryCommand([&] {
285 // Create and execute the SplitCommand
286 scene->receiveCommand(new SplitCommand(connectionToSplit, QPointF(x, y), scene));
287 return createSuccessResponse(QJsonObject(), requestId);
288 }, "split connection", requestId);
289}
290
291bool ConnectionHandler::resolvePort(const QJsonObject &params, const QString &prefix,
292 GraphicElement *element, bool isOutput,
293 int &portIndex, QString &errorMsg)
294{
295 const QString labelParam = prefix + "_port_label";
296 const QString indexParam = prefix + "_port";
297
298 if (!params.contains(labelParam) && !params.contains(indexParam)) {
299 errorMsg = QString("Missing %1 port: provide either '%2' (index) or '%3' (name)")
300 .arg(prefix, indexParam, labelParam);
301 return false;
302 }
303
304 if (params.contains(labelParam)) {
305 if (!validateNonEmptyString(params.value(labelParam), labelParam, errorMsg)) {
306 return false;
307 }
308 const QString label = params.value(labelParam).toString();
309 return isOutput ? outputPortByLabel(element, label, portIndex, errorMsg)
310 : inputPortByLabel(element, label, portIndex, errorMsg);
311 }
312
313 if (!validateNonNegativeInteger(params.value(indexParam), indexParam, errorMsg)) {
314 return false;
315 }
316 portIndex = params.value(indexParam).toInt();
317 return validatePortRange(element, portIndex, isOutput, indexParam, errorMsg);
318}
All QUndoCommand subclasses and the CommandUtils helper namespace.
ConnectionManager: manages wire creation, deletion, validation and hover feedback.
Connection: a wire that connects an output port to an input port in the circuit scene.
Singleton factory for all circuit element types.
Abstract base class for all graphical circuit elements.
Port classes: Port (base), InputPort, and OutputPort.
Main circuit editing scene with undo/redo and user interaction.
bool validatePortRange(GraphicElement *element, int portIndex, bool isOutput, const QString &paramName, QString &errorMsg) const
QJsonObject createSuccessResponse(const QJsonObject &result={}, const QJsonValue &requestId=QJsonValue()) const
QJsonObject tryCommand(Fn &&fn, const QString &action, const QJsonValue &requestId=QJsonValue())
Wraps fn in a try/catch, returning an error response on exception.
Definition BaseHandler.h:55
GraphicElement * validatedElement(const QJsonObject &params, const QString &paramName, QString &errorMsg)
Validates paramName in params, looks up the element, and returns it.
bool validateNonNegativeInteger(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
BaseHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject createErrorResponse(const QString &error, const QJsonValue &requestId=QJsonValue(), int code=JsonRpcError::InternalError) const
bool outputPortByLabel(GraphicElement *element, const QString &label, int &portIndex, QString &errorMsg) const
bool validateParameters(const QJsonObject &params, const QStringList &required) const
bool inputPortByLabel(GraphicElement *element, const QString &label, int &portIndex, QString &errorMsg) const
bool validateNonEmptyString(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
bool validateNumeric(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
Scene * currentScene()
QJsonObject handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId) override
ConnectionHandler(MainWindow *mainWindow, const MCPValidator *validator)
static bool isConnectionAllowed(OutputPort *startPort, InputPort *endPort)
Returns true if a wire from startPort to endPort is permitted.
Abstract base class for all graphical circuit elements in wiRedPanda.
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
JSON Schema validator for MCP commands and responses using native json-schema-validator.
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:47
GraphicElement * graphicElement()
Returns the graphic element that owns this port.
Definition Port.h:66
int index() const
Returns the port's visual/logical index within the element.
Definition Port.cpp:138
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
QList< QGraphicsItem * > items(Qt::SortOrder order=Qt::AscendingOrder) const
Definition Scene.cpp:718
constexpr int SceneNotAvailable
No active circuit scene to operate on.
constexpr int PortNotFound
Port lookup by index or label did not match.
constexpr int MethodNotFound
The requested method does not exist or is unavailable.
constexpr int ConnectionFailed
Connect/disconnect or port-mismatch failure.
constexpr int ElementNotFound
Referenced element id does not exist in the scene.
constexpr int ValidationError
Semantic validation failure (e.g. port index out of range, enum value not allowed).
constexpr int InvalidParams
Invalid method parameters (missing required, wrong type, etc.).