Skip to content

Commit f557e0a

Browse files
feat(simulator): add local socket backend for aux serial ports
Adds QLocalSocketBackend so user scripts can stand in for hardware on simulator aux serial ports. Backed by QLocalServer, which uses AF_UNIX on macOS / Linux and named pipes on Windows — no #ifdefs on our side. Single-client semantics: a second connection while one is active is rejected. The Configure Serial Ports dialog grows a per-port type selector (Not assigned / Serial port / Local socket) with a context- dependent input widget — combo for serial, line edit prefilled with edgetx-sim-aux<N> for sockets. While a socket is listening the dialog shows the resolved native path so users know where to point their script. HostSerialConnector::connectSerialPort is replaced with a generic connectBackend(index, kind, spec). Per-port encoding / baudrate are still cached on the connector and re-applied across backend swaps; setEncoding/setBaudrate are no-ops on the socket backend. Qt Network is now an explicit find_package COMPONENTS entry and linked into the common library. It was previously pulled in transitively by QNetworkAccessManager in updates/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3203720 commit f557e0a

11 files changed

Lines changed: 519 additions & 138 deletions

cmake/QtDefs.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ if(Qt6_FOUND)
1313
# call after Qt6Core package is found
1414
qt_standard_project_setup()
1515

16-
find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools Multimedia PrintSupport SerialPort Svg Xml)
16+
find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools Multimedia Network PrintSupport SerialPort Svg Xml)
1717

1818
### Get locations of Qt binary executables & libs (libs are for distros, not for linking)
1919
# first set up some hints

companion/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ target_link_libraries(common
189189
Qt::Xml
190190
Qt::Widgets
191191
Qt::SerialPort
192+
Qt::Network
192193
${SDL2_LIBRARIES}
193194
)
194195

companion/src/simulation/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ set(${PROJECT_NAME}_NAMES
2121
widgets/virtualjoystickwidget
2222
serialportsdialog
2323
hostserialbackend_serialport
24+
hostserialbackend_localsocket
2425
hostserialconnector
2526
simulateduiwidgetGeneric
2627
wasmsimulatorinterface
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#include "hostserialbackend_localsocket.h"
23+
24+
#include <QLocalServer>
25+
#include <QLocalSocket>
26+
#include <QtDebug>
27+
28+
QLocalSocketBackend::QLocalSocketBackend(const QString & socketName, QObject * parent)
29+
: HostSerialBackend(parent),
30+
socketName(socketName),
31+
server(new QLocalServer(this)),
32+
client(nullptr)
33+
{
34+
connect(server, &QLocalServer::newConnection,
35+
this, &QLocalSocketBackend::onNewConnection);
36+
}
37+
38+
QLocalSocketBackend::~QLocalSocketBackend()
39+
{
40+
close();
41+
}
42+
43+
bool QLocalSocketBackend::open()
44+
{
45+
if (server->isListening())
46+
return true;
47+
48+
// A previous run (or another process) may have left a stale socket
49+
// file behind on Unix; QLocalServer::removeServer is the documented
50+
// way to clear it before re-listening.
51+
QLocalServer::removeServer(socketName);
52+
53+
if (!server->listen(socketName)) {
54+
emit errorOccurred(tr("Failed to listen on local socket \"%1\": %2")
55+
.arg(socketName, server->errorString()));
56+
return false;
57+
}
58+
59+
qDebug() << "Listening on local socket" << server->fullServerName();
60+
return true;
61+
}
62+
63+
void QLocalSocketBackend::close()
64+
{
65+
if (client != nullptr) {
66+
client->disconnect(this);
67+
client->disconnectFromServer();
68+
client->deleteLater();
69+
client = nullptr;
70+
}
71+
if (server->isListening())
72+
server->close();
73+
}
74+
75+
bool QLocalSocketBackend::isOpen() const
76+
{
77+
return server->isListening();
78+
}
79+
80+
void QLocalSocketBackend::write(const QByteArray & data)
81+
{
82+
if (client != nullptr && client->state() == QLocalSocket::ConnectedState)
83+
client->write(data);
84+
}
85+
86+
QString QLocalSocketBackend::displayName() const
87+
{
88+
return socketName;
89+
}
90+
91+
QString QLocalSocketBackend::fullServerName() const
92+
{
93+
return server->isListening() ? server->fullServerName() : QString();
94+
}
95+
96+
void QLocalSocketBackend::onNewConnection()
97+
{
98+
while (server->hasPendingConnections()) {
99+
QLocalSocket * incoming = server->nextPendingConnection();
100+
if (client != nullptr) {
101+
// Single-client backend: politely refuse extra connections.
102+
incoming->disconnectFromServer();
103+
incoming->deleteLater();
104+
continue;
105+
}
106+
107+
client = incoming;
108+
connect(client, &QLocalSocket::readyRead,
109+
this, &QLocalSocketBackend::onClientReadyRead);
110+
connect(client, &QLocalSocket::disconnected,
111+
this, &QLocalSocketBackend::onClientDisconnected);
112+
qDebug() << "Local socket client connected on" << socketName;
113+
}
114+
}
115+
116+
void QLocalSocketBackend::onClientReadyRead()
117+
{
118+
if (client == nullptr)
119+
return;
120+
emit dataReceived(client->readAll());
121+
}
122+
123+
void QLocalSocketBackend::onClientDisconnected()
124+
{
125+
if (client == nullptr)
126+
return;
127+
client->deleteLater();
128+
client = nullptr;
129+
qDebug() << "Local socket client disconnected from" << socketName;
130+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#pragma once
23+
24+
#include "hostserialbackend.h"
25+
26+
class QLocalServer;
27+
class QLocalSocket;
28+
29+
// HostSerialBackend backed by a QLocalServer (Unix domain socket on
30+
// macOS / Linux, named pipe on Windows). Lets external scripts stand
31+
// in for hardware on a simulator aux serial port.
32+
//
33+
// Single-client semantics: while one client is connected, additional
34+
// connection attempts are accepted briefly and then closed. We use
35+
// last-write-wins for outgoing data — bytes from the simulator only
36+
// reach the active client.
37+
class QLocalSocketBackend : public HostSerialBackend
38+
{
39+
Q_OBJECT
40+
41+
public:
42+
QLocalSocketBackend(const QString & socketName, QObject * parent = nullptr);
43+
~QLocalSocketBackend() override;
44+
45+
bool open() override;
46+
void close() override;
47+
bool isOpen() const override;
48+
49+
void write(const QByteArray & data) override;
50+
51+
QString displayName() const override;
52+
53+
// Resolved native path / pipe name (e.g.
54+
// /var/folders/.../edgetx-sim-aux1 or
55+
// \\.\pipe\edgetx-sim-aux1). Empty when the server is not
56+
// listening.
57+
QString fullServerName() const;
58+
59+
private slots:
60+
void onNewConnection();
61+
void onClientReadyRead();
62+
void onClientDisconnected();
63+
64+
private:
65+
QString socketName;
66+
QLocalServer * server;
67+
QLocalSocket * client;
68+
};

companion/src/simulation/hostserialconnector.cpp

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323

2424
#include "hostserialbackend.h"
2525
#include "hostserialbackend_serialport.h"
26+
#include "hostserialbackend_localsocket.h"
2627

2728
HostSerialConnector::HostSerialConnector(QObject *parent, SimulatorInterface *simulator)
2829
: QObject(parent),
2930
simulator(simulator)
3031
{
3132
for (int i = 0; i < MAX_HOST_SERIAL; i++) {
3233
hostAuxBackends[i] = nullptr;
34+
hostAuxBackendKinds[i] = BackendNone;
3335
hostAuxPortsEncoding[i] = SERIAL_ENCODING_8N1;
3436
hostAuxPortsBaudRate[i] = 9600;
3537
hostAuxPortsOpen[i] = false;
@@ -46,21 +48,44 @@ HostSerialConnector::~HostSerialConnector()
4648
}
4749
}
4850

49-
QString HostSerialConnector::getConnectedSerialPortName(int index)
51+
HostSerialConnector::BackendKind HostSerialConnector::getBackendKind(int index) const
5052
{
5153
if (index >= MAX_HOST_SERIAL)
52-
return QString("");
54+
return BackendNone;
55+
56+
QMutexLocker locker(&hostAuxPortsMutex);
57+
return hostAuxBackendKinds[index];
58+
}
59+
60+
QString HostSerialConnector::getBackendSpec(int index) const
61+
{
62+
if (index >= MAX_HOST_SERIAL)
63+
return QString();
5364

5465
QMutexLocker locker(&hostAuxPortsMutex);
5566

5667
HostSerialBackend * backend = hostAuxBackends[index];
5768
if (backend == nullptr)
58-
return QString("");
69+
return QString();
5970

6071
return backend->displayName();
6172
}
6273

63-
void HostSerialConnector::setBackend(int index, HostSerialBackend * backend)
74+
QString HostSerialConnector::getLocalSocketFullName(int index) const
75+
{
76+
if (index >= MAX_HOST_SERIAL)
77+
return QString();
78+
79+
QMutexLocker locker(&hostAuxPortsMutex);
80+
81+
if (hostAuxBackendKinds[index] != BackendLocalSocket)
82+
return QString();
83+
84+
auto * backend = qobject_cast<QLocalSocketBackend *>(hostAuxBackends[index]);
85+
return backend != nullptr ? backend->fullServerName() : QString();
86+
}
87+
88+
void HostSerialConnector::setBackend(int index, HostSerialBackend * backend, BackendKind kind)
6489
{
6590
// Caller holds hostAuxPortsMutex.
6691
HostSerialBackend * old = hostAuxBackends[index];
@@ -70,6 +95,7 @@ void HostSerialConnector::setBackend(int index, HostSerialBackend * backend)
7095
}
7196

7297
hostAuxBackends[index] = backend;
98+
hostAuxBackendKinds[index] = backend != nullptr ? kind : BackendNone;
7399

74100
if (backend == nullptr)
75101
return;
@@ -88,19 +114,31 @@ void HostSerialConnector::setBackend(int index, HostSerialBackend * backend)
88114
backend->setBaudrate(hostAuxPortsBaudRate[index]);
89115
}
90116

91-
void HostSerialConnector::connectSerialPort(int index, QString portName)
117+
void HostSerialConnector::connectBackend(int index, int kind, const QString & spec)
92118
{
93119
if (index >= MAX_HOST_SERIAL)
94120
return;
95121

96122
QMutexLocker locker(&hostAuxPortsMutex);
97123

98-
if (portName.isEmpty()) {
99-
setBackend(index, nullptr);
124+
if (kind == BackendNone || spec.isEmpty()) {
125+
setBackend(index, nullptr, BackendNone);
100126
return;
101127
}
102128

103-
setBackend(index, new QSerialPortBackend(portName, this));
129+
HostSerialBackend * backend = nullptr;
130+
switch (kind) {
131+
case BackendSerialPort:
132+
backend = new QSerialPortBackend(spec, this);
133+
break;
134+
case BackendLocalSocket:
135+
backend = new QLocalSocketBackend(spec, this);
136+
break;
137+
default:
138+
return;
139+
}
140+
141+
setBackend(index, backend, static_cast<BackendKind>(kind));
104142

105143
if (hostAuxPortsOpen[index])
106144
serialStart(index);

companion/src/simulation/hostserialconnector.h

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,28 @@ class HostSerialConnector : public QObject
3939
Q_OBJECT
4040

4141
public:
42+
enum BackendKind {
43+
BackendNone = 0,
44+
BackendSerialPort,
45+
BackendLocalSocket,
46+
};
47+
Q_ENUM(BackendKind)
48+
4249
explicit HostSerialConnector(QObject * parent, SimulatorInterface * simulator);
4350
~HostSerialConnector();
4451

45-
QString getConnectedSerialPortName(int index);
52+
BackendKind getBackendKind(int index) const;
53+
QString getBackendSpec(int index) const;
54+
55+
// Convenience: only meaningful when the active backend is a
56+
// QLocalSocketBackend. Returns an empty string otherwise.
57+
QString getLocalSocketFullName(int index) const;
4658

4759
public slots:
48-
void connectSerialPort(int index, QString portName);
60+
// kind=BackendNone or empty spec disconnects the port. For
61+
// BackendSerialPort, spec is the device name (e.g. "ttyUSB0");
62+
// for BackendLocalSocket, spec is the QLocalServer listen name.
63+
void connectBackend(int index, int kind, const QString & spec);
4964
void sendSerialData(const quint8 index, const QByteArray & data);
5065
void setSerialEncoding(const quint8 index, const quint8 encoding);
5166
void setSerialBaudRate(const quint8 index, const quint32 baudrate);
@@ -58,12 +73,13 @@ class HostSerialConnector : public QObject
5873
void backendError(int index, const QString & message);
5974

6075
private:
61-
void setBackend(int index, HostSerialBackend * backend);
76+
void setBackend(int index, HostSerialBackend * backend, BackendKind kind);
6277

6378
SimulatorInterface * simulator;
6479

65-
QRecursiveMutex hostAuxPortsMutex;
80+
mutable QRecursiveMutex hostAuxPortsMutex;
6681
HostSerialBackend * hostAuxBackends[MAX_HOST_SERIAL];
82+
BackendKind hostAuxBackendKinds[MAX_HOST_SERIAL];
6783
quint8 hostAuxPortsEncoding[MAX_HOST_SERIAL];
6884
quint32 hostAuxPortsBaudRate[MAX_HOST_SERIAL];
6985
bool hostAuxPortsOpen[MAX_HOST_SERIAL];

0 commit comments

Comments
 (0)