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 // Unreachable: resolvePort() only returns true once validatePortRange()/label lookup
71 // already confirmed a real, in-range port -- outputPort()/inputPort() can't fail for it.
72 if (!outputPort || !inputPort) {
73 return createErrorResponse("Invalid port specification", requestId, JsonRpcError::PortNotFound); // LCOV_EXCL_LINE
74 }
75
76 auto *startPort = dynamic_cast<OutputPort *>(outputPort);
77 auto *endPort = dynamic_cast<InputPort *>(inputPort);
78
79 // Enforce the same rules the UI applies on wire drop (F21): no duplicate
80 // connections, no second driver on an occupied input, no wires onto
81 // wireless Tx/Rx ports. Without this, MCP could build circuits the UI
82 // forbids (the simulator degrades them to Error status).
83 if (!ConnectionManager::isConnectionAllowed(startPort, endPort)) {
84 return createErrorResponse(QString("Connection from element %1 port %2 to element %3 port %4 is not allowed " // LCOV_EXCL_LINE -- pattern 8/45: gcov misattributes this multi-line chained-.arg() call's first line even though it's genuinely reached
85 "(duplicate, occupied input, or wireless port)")
86 .arg(sourceElement->id()).arg(sourcePort).arg(targetElement->id()).arg(targetPort),
88 }
89
90 Scene *scene = currentScene();
91 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
92 // this exact message) if there's no active scene, before this point can ever be reached.
93 if (!scene) {
94 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
95 }
96
97 auto connection = std::make_unique<Connection>();
98 connection->setStartPort(startPort);
99 connection->setEndPort(endPort);
100 connection->updatePath();
101
102 // Unreachable: AddItemsCommand::redo() only calls CommandUtils::loadItems() and
103 // Scene::setCircuitUpdateRequired(), neither of which throws for an already-validated
104 // Connection (confirmed exception-free for this exact code path across the whole
105 // App/Scene/Commands.cpp sweep). Kept as a defensive backstop.
106 try {
107 scene->receiveCommand(new AddItemsCommand({connection.get()}, scene));
108 connection.release(); // scene/command takes ownership
109 } catch (const std::exception &e) { // LCOV_EXCL_LINE
110 return createErrorResponse(QString("Failed to connect elements: %1").arg(e.what()), // LCOV_EXCL_LINE
111 requestId, JsonRpcError::ConnectionFailed); // LCOV_EXCL_LINE
112 } catch (...) { // LCOV_EXCL_LINE
113 return createErrorResponse("Failed to connect elements: Unknown exception", // LCOV_EXCL_LINE
114 requestId, JsonRpcError::ConnectionFailed); // LCOV_EXCL_LINE
115 } // LCOV_EXCL_LINE
116
117 return createSuccessResponse(QJsonObject(), requestId);
118}
119
120QJsonObject ConnectionHandler::handleDisconnectElements(const QJsonObject &params, const QJsonValue &requestId)
121{
122 if (!validateParameters(params, {"source_id", "target_id"})) {
123 return createErrorResponse("Missing required parameters: source_id, target_id", requestId, JsonRpcError::InvalidParams);
124 }
125
126 QString errorMsg;
127 auto *sourceElement = validatedElement(params, "source_id", errorMsg);
128 if (!sourceElement) {
129 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
130 }
131 auto *targetElement = validatedElement(params, "target_id", errorMsg);
132 if (!targetElement) {
133 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
134 }
135
136 Scene *scene = currentScene();
137 // Unreachable: validatedElement() above already fails first (returning ElementNotFound,
138 // with this exact message) if there's no active scene.
139 if (!scene) {
140 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
141 }
142
143 const auto connections = scene->items();
144 for (auto *item : connections) {
145 auto *connection = qgraphicsitem_cast<Connection *>(item);
146 if (!connection) {
147 continue;
148 }
149
150 Port *port1 = connection->startPort();
151 Port *port2 = connection->endPort();
152
153 if (!port1 || !port2) {
154 continue;
155 }
156
157 GraphicElement *elem1 = port1->graphicElement();
158 GraphicElement *elem2 = port2->graphicElement();
159
160 if ((elem1 == sourceElement && elem2 == targetElement) ||
161 (elem1 == targetElement && elem2 == sourceElement)) {
162 return tryCommand([&] { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
163 scene->receiveCommand(new DeleteItemsCommand({connection}, scene));
164 return createSuccessResponse(QJsonObject(), requestId);
165 }, "disconnect elements", requestId);
166 }
167 }
168
169 return createErrorResponse(QString("No connection found between elements %1 and %2").arg(sourceElement->id()).arg(targetElement->id()),
171}
172
173QJsonObject ConnectionHandler::handleListConnections(const QJsonObject &, const QJsonValue &requestId)
174{
175 Scene *scene = currentScene();
176 if (!scene) {
177 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
178 }
179
180 QJsonArray connections;
181 const auto sceneItems = scene->items();
182
183 for (const auto *item : sceneItems) {
184 const auto *connection = qgraphicsitem_cast<const Connection *>(item);
185 if (!connection) {
186 continue;
187 }
188
189 const Port *startPort = connection->startPort();
190 const Port *endPort = connection->endPort();
191
192 if (!startPort || !endPort) {
193 continue;
194 }
195
196 const GraphicElement *startElement = startPort->graphicElement();
197 const GraphicElement *endElement = endPort->graphicElement();
198
199 // Unreachable: ElementPorts::addPort() unconditionally binds a port to its owning
200 // element at construction, so graphicElement() is never null for a real port.
201 if (!startElement || !endElement) {
202 continue; // LCOV_EXCL_LINE
203 }
204
205 QJsonObject connectionObj;
206 connectionObj["source_id"] = startElement->id();
207 connectionObj["source_port"] = startPort->index();
208 connectionObj["target_id"] = endElement->id();
209 connectionObj["target_port"] = endPort->index();
210
211 connections.append(connectionObj);
212 }
213
214 QJsonObject result;
215 result["connections"] = connections;
216
217 return createSuccessResponse(result, requestId);
218}
219
220QJsonObject ConnectionHandler::handleSplitConnection(const QJsonObject &params, const QJsonValue &requestId)
221{
222 if (!validateParameters(params, {"source_id", "source_port", "target_id", "target_port", "x", "y"})) {
223 return createErrorResponse("Missing required parameters: source_id, source_port, target_id, target_port, x, y",
224 requestId, JsonRpcError::InvalidParams);
225 }
226
227 QString errorMsg;
228
229 if (!validateNonNegativeInteger(params.value("source_port"), "source_port", errorMsg)) {
230 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
231 }
232 if (!validateNonNegativeInteger(params.value("target_port"), "target_port", errorMsg)) {
233 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
234 }
235 if (!validateNumeric(params.value("x"), "x", errorMsg)) {
236 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
237 }
238 if (!validateNumeric(params.value("y"), "y", errorMsg)) {
239 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
240 }
241
242 const int sourcePort = params.value("source_port").toInt();
243 const int targetPort = params.value("target_port").toInt();
244 const double x = params.value("x").toDouble();
245 const double y = params.value("y").toDouble();
246
247 auto *sourceElement = validatedElement(params, "source_id", errorMsg);
248 if (!sourceElement) {
249 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
250 }
251 auto *targetElement = validatedElement(params, "target_id", errorMsg);
252 if (!targetElement) {
253 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
254 }
255
256 Scene *scene = currentScene();
257 // Unreachable: validatedElement() above already fails first (returning ElementNotFound,
258 // with this exact message) if there's no active scene.
259 if (!scene) {
260 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
261 }
262
263 // Find the connection between source and target
264 Connection *connectionToSplit = nullptr;
265 const auto sceneItems = scene->items();
266
267 for (auto *item : sceneItems) {
268 auto *connection = qgraphicsitem_cast<Connection *>(item);
269 if (!connection) { continue; }
270
271 Port *port1 = connection->startPort();
272 Port *port2 = connection->endPort();
273
274 if (!port1 || !port2) {
275 continue;
276 }
277
278 GraphicElement *elem1 = port1->graphicElement();
279 GraphicElement *elem2 = port2->graphicElement();
280
281 // Unreachable: same port-always-has-an-owner invariant as handleListConnections().
282 if (!elem1 || !elem2) {
283 continue; // LCOV_EXCL_LINE
284 }
285
286 // Check if this connection matches source->target
287 if (elem1 == sourceElement && elem2 == targetElement &&
288 port1->index() == sourcePort && port2->index() == targetPort) {
289 connectionToSplit = connection;
290 break;
291 }
292 }
293
294 if (!connectionToSplit) {
295 return createErrorResponse("Connection not found between specified source and target",
297 }
298
299 return tryCommand([&] { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
300 // Create and execute the SplitCommand
301 scene->receiveCommand(new SplitCommand(connectionToSplit, QPointF(x, y), scene));
302 return createSuccessResponse(QJsonObject(), requestId);
303 }, "split connection", requestId);
304}
305
306bool ConnectionHandler::resolvePort(const QJsonObject &params, const QString &prefix,
307 GraphicElement *element, bool isOutput,
308 int &portIndex, QString &errorMsg)
309{
310 const QString labelParam = prefix + "_port_label";
311 const QString indexParam = prefix + "_port";
312
313 if (!params.contains(labelParam) && !params.contains(indexParam)) {
314 errorMsg = QString("Missing %1 port: provide either '%2' (index) or '%3' (name)")
315 .arg(prefix, indexParam, labelParam);
316 return false;
317 }
318
319 if (params.contains(labelParam)) {
320 if (!validateNonEmptyString(params.value(labelParam), labelParam, errorMsg)) {
321 return false;
322 }
323 const QString label = params.value(labelParam).toString();
324 return isOutput ? outputPortByLabel(element, label, portIndex, errorMsg)
325 : inputPortByLabel(element, label, portIndex, errorMsg);
326 }
327
328 if (!validateNonNegativeInteger(params.value(indexParam), indexParam, errorMsg)) {
329 return false;
330 }
331 portIndex = params.value(indexParam).toInt();
332 return validatePortRange(element, portIndex, isOutput, indexParam, errorMsg);
333}
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:46
GraphicElement * graphicElement()
Returns the graphic element that owns this port.
Definition Port.h:71
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.).