Import existing sources
This commit is contained in:
331
espremoteagent/espremoteagent.cpp
Normal file
331
espremoteagent/espremoteagent.cpp
Normal file
@ -0,0 +1,331 @@
|
||||
#include "espremoteagent.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "webservercontainer.h"
|
||||
#include "webserverclientconnection.h"
|
||||
#include "espremoteagentcontainers.h"
|
||||
#include "espremoteport.h"
|
||||
|
||||
EspRemoteAgent::EspRemoteAgent(std::vector<SerialPortConfig> &&serialPortConfigs, QObject *parent) :
|
||||
AbstractWebserver{parent}
|
||||
{
|
||||
m_ports.reserve(serialPortConfigs.size());
|
||||
|
||||
for (auto &config : serialPortConfigs)
|
||||
m_ports.emplace_back(std::make_unique<EspRemotePort>(std::move(config), this));
|
||||
}
|
||||
|
||||
EspRemoteAgent::~EspRemoteAgent() = default;
|
||||
|
||||
void EspRemoteAgent::requestReceived(WebserverClientConnection &client, const Request &request)
|
||||
{
|
||||
const QUrl url{request.path};
|
||||
const QUrlQuery query{url};
|
||||
|
||||
if (url.path() == "/")
|
||||
{
|
||||
sendRootResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/open")
|
||||
{
|
||||
sendOpenResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/close")
|
||||
{
|
||||
sendCloseResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/reboot")
|
||||
{
|
||||
sendRebootResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/setDTR")
|
||||
{
|
||||
sendSetDTRResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/setRTS")
|
||||
{
|
||||
sendSetRTSResponse(client, url, query);
|
||||
}
|
||||
else
|
||||
if (!client.sendFullResponse(404, "Not Found", {{"Content-Type", "text/plain"}}, "The requested path \"" + request.path + "\" was not found."))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendRootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
QString content =
|
||||
"<html>"
|
||||
"<head>"
|
||||
"<title>ESP Remote Agent</title>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<table border=\"1\">"
|
||||
"<thead>"
|
||||
"<tr>"
|
||||
"<th>ID</th>"
|
||||
"<th>Port</th>"
|
||||
"<th>Status</th>"
|
||||
"<th>Message</th>"
|
||||
"<th>Actions</th>"
|
||||
"<th>Log output</th>"
|
||||
"</tr>"
|
||||
"</thead>"
|
||||
"<tbody>";
|
||||
|
||||
std::size_t i{};
|
||||
for (const auto &port : m_ports)
|
||||
{
|
||||
const auto currentId = i++;
|
||||
content += QStringLiteral("<tr>"
|
||||
"<td>%0</td>"
|
||||
"<td>%1</td>"
|
||||
"<td>%2</td>"
|
||||
"<td>%3</td>"
|
||||
"<td>"
|
||||
"<a href=\"open?id=%0\">Open</a> "
|
||||
"<a href=\"close?id=%0\">Close</a><br />"
|
||||
"<a href=\"reboot?id=%0\">Reboot</a><br />"
|
||||
"DTR %4 <a href=\"setDTR?id=%0&set=%5\">Toggle</a><br />"
|
||||
"RTS %6 <a href=\"setRTS?id=%0&set=%7\">Toggle</a>"
|
||||
"</td>"
|
||||
"<td><pre>%8</pre></td>"
|
||||
"</tr>")
|
||||
.arg(currentId)
|
||||
.arg(port->port())
|
||||
.arg(port->status())
|
||||
.arg(port->message())
|
||||
.arg(port->isDataTerminalReady() ? "On" : "Off")
|
||||
.arg(port->isDataTerminalReady() ? "false" : "true")
|
||||
.arg(port->isRequestToSend() ? "On" : "Off")
|
||||
.arg(port->isRequestToSend() ? "false" : "true")
|
||||
.arg(port->logOutput());
|
||||
}
|
||||
|
||||
content += "</tbody>"
|
||||
"</table>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/html"}}, content.toUtf8()))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendOpenResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("id"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto idStr = query.queryItemValue("id");
|
||||
bool ok{};
|
||||
const auto id = idStr.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse id"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
if (id < 0 || id >= m_ports.size())
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id out of range"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((*std::next(std::begin(m_ports), id))->tryOpen())
|
||||
{
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "Port opened successfully!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(500, "Internal Server Error", {{"Content-Type", "text/plain"}}, "Opening port failed!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendCloseResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("id"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto idStr = query.queryItemValue("id");
|
||||
bool ok{};
|
||||
const auto id = idStr.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse id"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
if (id < 0 || id >= m_ports.size())
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id out of range"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
(*std::next(std::begin(m_ports), id))->close();
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "Port closed successfully!"))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendRebootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("id"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto idStr = query.queryItemValue("id");
|
||||
bool ok{};
|
||||
const auto id = idStr.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse id"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
if (id < 0 || id >= m_ports.size())
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id out of range"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((*std::next(std::begin(m_ports), id))->reboot())
|
||||
{
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "Reboot successfully!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(500, "Internal Server Error", {{"Content-Type", "text/plain"}}, "Reboot failed!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendSetDTRResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("id"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto idStr = query.queryItemValue("id");
|
||||
bool ok{};
|
||||
const auto id = idStr.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse id"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
if (id < 0 || id >= m_ports.size())
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id out of range"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.hasQueryItem("set"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "set missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
bool set;
|
||||
if (const auto setStr = query.queryItemValue("set"); setStr == "true")
|
||||
set = true;
|
||||
else if (setStr == "false")
|
||||
set = false;
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse set"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((*std::next(std::begin(m_ports), id))->setDataTerminalReady(set))
|
||||
{
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "Port set DTR successfully!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(500, "Internal Server Error", {{"Content-Type", "text/plain"}}, "Set port DTR failed!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EspRemoteAgent::sendSetRTSResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("id"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto idStr = query.queryItemValue("id");
|
||||
bool ok{};
|
||||
const auto id = idStr.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse id"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
if (id < 0 || id >= m_ports.size())
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "id out of range"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.hasQueryItem("set"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "set missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
bool set;
|
||||
if (const auto setStr = query.queryItemValue("set"); setStr == "true")
|
||||
set = true;
|
||||
else if (setStr == "false")
|
||||
set = false;
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "could not parse set"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((*std::next(std::begin(m_ports), id))->setRequestToSend(set))
|
||||
{
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "Port set RTS successfully!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!client.sendFullResponse(500, "Internal Server Error", {{"Content-Type", "text/plain"}}, "Set port RTS failed!"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
}
|
35
espremoteagent/espremoteagent.h
Normal file
35
espremoteagent/espremoteagent.h
Normal file
@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include "abstractwebserver.h"
|
||||
|
||||
class SerialPortConfig;
|
||||
class WebserverClientConnection;
|
||||
class Request;
|
||||
class EspRemotePort;
|
||||
class QUrl;
|
||||
class QUrlQuery;
|
||||
|
||||
class EspRemoteAgent : public AbstractWebserver
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EspRemoteAgent(std::vector<SerialPortConfig> &&serialPortConfigs, QObject *parent = nullptr);
|
||||
~EspRemoteAgent() override;
|
||||
|
||||
protected:
|
||||
void requestReceived(WebserverClientConnection &client, const Request &request) override;
|
||||
|
||||
private:
|
||||
void sendRootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendOpenResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendCloseResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendRebootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendSetDTRResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendSetRTSResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
|
||||
std::vector<std::unique_ptr<EspRemotePort>> m_ports;
|
||||
};
|
13
espremoteagent/espremoteagent.ini
Normal file
13
espremoteagent/espremoteagent.ini
Normal file
@ -0,0 +1,13 @@
|
||||
[Webserver]
|
||||
listen=Any
|
||||
port=80
|
||||
|
||||
[PortA]
|
||||
port=/dev/ttyUSB0
|
||||
baudrate=115200
|
||||
url=ws://office-pi:1235/charger0
|
||||
|
||||
[PortB]
|
||||
port=/dev/ttyUSB1
|
||||
baudrate=115200
|
||||
url=ws://office-pi:1235/charger1
|
31
espremoteagent/espremoteagent.pro
Normal file
31
espremoteagent/espremoteagent.pro
Normal file
@ -0,0 +1,31 @@
|
||||
QT = core network serialport websockets
|
||||
|
||||
TARGET = espremoteagent
|
||||
|
||||
TEMPLATE = app
|
||||
|
||||
CONFIG += console
|
||||
CONFIG -= app_bundle
|
||||
|
||||
PROJECT_ROOT = ..
|
||||
|
||||
DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/bin
|
||||
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
DBLIBS += webserver
|
||||
|
||||
HEADERS += \
|
||||
espremoteagent.h \
|
||||
espremoteagentcontainers.h \
|
||||
espremoteport.h
|
||||
|
||||
SOURCES += \
|
||||
espremoteport.cpp \
|
||||
main.cpp \
|
||||
espremoteagent.cpp
|
||||
|
||||
OTHER_FILES += \
|
||||
espremoteagent.ini
|
||||
|
||||
include($${PROJECT_ROOT}/project.pri)
|
11
espremoteagent/espremoteagentcontainers.h
Normal file
11
espremoteagent/espremoteagentcontainers.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
struct SerialPortConfig
|
||||
{
|
||||
QString port;
|
||||
int baudrate;
|
||||
QUrl url;
|
||||
};
|
225
espremoteagent/espremoteport.cpp
Normal file
225
espremoteagent/espremoteport.cpp
Normal file
@ -0,0 +1,225 @@
|
||||
#include "espremoteport.h"
|
||||
|
||||
#include <QSerialPort>
|
||||
#include <QWebSocket>
|
||||
#include <QTimerEvent>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
EspRemotePort::EspRemotePort(SerialPortConfig &&config, QObject *parent) :
|
||||
QObject{parent},
|
||||
m_config{std::move(config)},
|
||||
m_port{std::make_unique<QSerialPort>(m_config.port)},
|
||||
m_websocket{m_config.url.isEmpty() ? nullptr : std::make_unique<QWebSocket>(QString{}, QWebSocketProtocol::VersionLatest, this)}
|
||||
{
|
||||
connect(m_port.get(), &QSerialPort::readyRead, this, &EspRemotePort::serialReadyRead);
|
||||
|
||||
if (m_websocket)
|
||||
{
|
||||
connect(m_websocket.get(), &QWebSocket::connected, this, &EspRemotePort::websocketConnected);
|
||||
connect(m_websocket.get(), &QWebSocket::disconnected, this, &EspRemotePort::websocketDisconnected);
|
||||
connect(m_websocket.get(), qOverload<QAbstractSocket::SocketError>(&QWebSocket::error), this, &EspRemotePort::websocketError);
|
||||
connect(m_websocket.get(), &QWebSocket::textMessageReceived, this, &EspRemotePort::websocketTextMessageReceived);
|
||||
qDebug() << "connecting to" << m_config.url;
|
||||
m_websocket->open(m_config.url);
|
||||
}
|
||||
|
||||
tryOpen();
|
||||
}
|
||||
|
||||
EspRemotePort::~EspRemotePort() = default;
|
||||
|
||||
QString EspRemotePort::status() const
|
||||
{
|
||||
if (m_port->isOpen())
|
||||
return tr("Open");
|
||||
else
|
||||
return tr("Not open");
|
||||
}
|
||||
|
||||
QString EspRemotePort::logOutput() const
|
||||
{
|
||||
QString str;
|
||||
for (const auto &line : m_logOutput)
|
||||
{
|
||||
if (!str.isEmpty())
|
||||
str += "\n";
|
||||
str += line.toHtmlEscaped();
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
bool EspRemotePort::reboot()
|
||||
{
|
||||
bool set = m_port->isDataTerminalReady();
|
||||
|
||||
Q_ASSERT(m_port);
|
||||
|
||||
if (!m_port->setDataTerminalReady(!set))
|
||||
return false;
|
||||
|
||||
QTimer::singleShot(100, m_port.get(), [set,port=m_port.get()](){
|
||||
if (!port->setDataTerminalReady(set))
|
||||
qWarning() << "reboot failed";
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EspRemotePort::setDataTerminalReady(bool set)
|
||||
{
|
||||
return m_port->setDataTerminalReady(set);
|
||||
}
|
||||
|
||||
bool EspRemotePort::isDataTerminalReady()
|
||||
{
|
||||
return m_port->isDataTerminalReady();
|
||||
}
|
||||
|
||||
bool EspRemotePort::setRequestToSend(bool set)
|
||||
{
|
||||
return m_port->setRequestToSend(set);
|
||||
}
|
||||
|
||||
bool EspRemotePort::isRequestToSend()
|
||||
{
|
||||
return m_port->isRequestToSend();
|
||||
}
|
||||
|
||||
bool EspRemotePort::tryOpen()
|
||||
{
|
||||
m_port->close();
|
||||
|
||||
if (!m_port->setBaudRate(m_config.baudrate))
|
||||
qWarning() << "could not set baud rate" << m_config.baudrate;
|
||||
|
||||
if (!m_port->open(QIODevice::ReadWrite))
|
||||
{
|
||||
m_message = tr("Could not open port because %0").arg(m_port->errorString());
|
||||
qWarning() << m_message;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EspRemotePort::close()
|
||||
{
|
||||
m_port->close();
|
||||
}
|
||||
|
||||
void EspRemotePort::timerEvent(QTimerEvent *event)
|
||||
{
|
||||
if (event->timerId() == m_reconnectTimerId)
|
||||
{
|
||||
m_reconnectTimerId = -1;
|
||||
|
||||
if (!m_config.url.isEmpty())
|
||||
{
|
||||
Q_ASSERT(m_websocket);
|
||||
qDebug() << "reconnecting to" << m_config.url;
|
||||
m_websocket->open(m_config.url);
|
||||
}
|
||||
}
|
||||
else
|
||||
QObject::timerEvent(event);
|
||||
}
|
||||
|
||||
void EspRemotePort::serialReadyRead()
|
||||
{
|
||||
while (m_port->canReadLine())
|
||||
{
|
||||
auto line = m_port->readLine();
|
||||
|
||||
if (line.endsWith('\n'))
|
||||
{
|
||||
line.chop(1);
|
||||
if (line.endsWith('\r'))
|
||||
line.chop(1);
|
||||
}
|
||||
|
||||
// qDebug() << line;
|
||||
|
||||
if (m_websocket)
|
||||
{
|
||||
m_websocket->sendTextMessage(QJsonDocument{QJsonObject{
|
||||
{"type", "log"},
|
||||
{"line", QString{line}},
|
||||
}}.toJson());
|
||||
}
|
||||
|
||||
m_logOutput.push(std::move(line));
|
||||
|
||||
while (m_logOutput.size() > 10)
|
||||
m_logOutput.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void EspRemotePort::websocketConnected()
|
||||
{
|
||||
qDebug() << "called";
|
||||
|
||||
if (m_reconnectTimerId != -1)
|
||||
killTimer(m_reconnectTimerId);
|
||||
}
|
||||
|
||||
void EspRemotePort::websocketDisconnected()
|
||||
{
|
||||
qDebug() << "called";
|
||||
}
|
||||
|
||||
void EspRemotePort::websocketError(QAbstractSocket::SocketError error)
|
||||
{
|
||||
qDebug() << "called" << error;
|
||||
|
||||
if (m_reconnectTimerId != -1)
|
||||
killTimer(m_reconnectTimerId);
|
||||
|
||||
m_reconnectTimerId = startTimer(5000);
|
||||
}
|
||||
|
||||
void EspRemotePort::websocketTextMessageReceived(const QString &message)
|
||||
{
|
||||
// qDebug() << message;
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "could not parse json command:" << error.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isObject())
|
||||
{
|
||||
qWarning() << "json command is not an object";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto obj = doc.object();
|
||||
|
||||
if (!obj.contains("type"))
|
||||
{
|
||||
qWarning() << "json command does not contain a type";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto typeVal = obj.value("type");
|
||||
|
||||
if (!typeVal.isString())
|
||||
{
|
||||
qWarning() << "json command type is not a string";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto type = typeVal.toString();
|
||||
|
||||
if (type == "reboot")
|
||||
{
|
||||
reboot();
|
||||
}
|
||||
else
|
||||
qWarning() << "unknown command type" << type;
|
||||
}
|
56
espremoteagent/espremoteport.h
Normal file
56
espremoteagent/espremoteport.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractSocket>
|
||||
|
||||
#include "espremoteagentcontainers.h"
|
||||
#include "webserverutils.h"
|
||||
|
||||
class QSerialPort;
|
||||
class QWebSocket;
|
||||
|
||||
class EspRemotePort : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EspRemotePort(SerialPortConfig &&config, QObject *parent = nullptr);
|
||||
~EspRemotePort() override;
|
||||
|
||||
QString port() const { return m_config.port; }
|
||||
QString status() const;
|
||||
QString message() const { return m_message; }
|
||||
QString logOutput() const;
|
||||
|
||||
bool reboot();
|
||||
|
||||
bool setDataTerminalReady(bool set);
|
||||
bool isDataTerminalReady();
|
||||
|
||||
bool setRequestToSend(bool set);
|
||||
bool isRequestToSend();
|
||||
|
||||
bool tryOpen();
|
||||
void close();
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void serialReadyRead();
|
||||
void websocketConnected();
|
||||
void websocketDisconnected();
|
||||
void websocketError(QAbstractSocket::SocketError error);
|
||||
void websocketTextMessageReceived(const QString &message);
|
||||
|
||||
private:
|
||||
SerialPortConfig m_config;
|
||||
const std::unique_ptr<QSerialPort> m_port;
|
||||
const std::unique_ptr<QWebSocket> m_websocket;
|
||||
QString m_message;
|
||||
iterable_queue<QString> m_logOutput;
|
||||
|
||||
int m_reconnectTimerId{-1};
|
||||
};
|
67
espremoteagent/main.cpp
Normal file
67
espremoteagent/main.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QSettings>
|
||||
#include <QUrl>
|
||||
|
||||
#include "espremoteagent.h"
|
||||
#include "espremoteagentcontainers.h"
|
||||
#include "webserverutils.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} "
|
||||
"["
|
||||
"%{if-debug}D%{endif}"
|
||||
"%{if-info}I%{endif}"
|
||||
"%{if-warning}W%{endif}"
|
||||
"%{if-critical}C%{endif}"
|
||||
"%{if-fatal}F%{endif}"
|
||||
"] "
|
||||
"%{function}(): "
|
||||
"%{message}"));
|
||||
|
||||
QCoreApplication app{argc, argv};
|
||||
|
||||
QSettings settings{"espremoteagent.ini", QSettings::IniFormat};
|
||||
|
||||
std::vector<SerialPortConfig> serialPortConfigs;
|
||||
|
||||
const auto probePort = [&](auto group){
|
||||
auto port = settings.value(QStringLiteral("%0/port").arg(group)).toString();
|
||||
if (port.isEmpty())
|
||||
return;
|
||||
|
||||
QUrl url;
|
||||
if (auto urlStr = settings.value(QStringLiteral("%0/url").arg(group)).toString(); !urlStr.isEmpty())
|
||||
url = QUrl{std::move(urlStr)};
|
||||
|
||||
int baudrate{};
|
||||
bool ok{};
|
||||
baudrate = settings.value(QStringLiteral("%0/baudrate").arg(group)).toInt(&ok);
|
||||
if (!ok)
|
||||
qFatal("could not parse baudrate for %s", qPrintable(group));
|
||||
|
||||
serialPortConfigs.emplace_back(SerialPortConfig{
|
||||
.port=std::move(port),
|
||||
.baudrate=baudrate,
|
||||
.url=std::move(url)
|
||||
});
|
||||
};
|
||||
|
||||
probePort("PortA");
|
||||
probePort("PortB");
|
||||
|
||||
QHostAddress webserverListen = parseHostAddress(settings.value("Webserver/listen").toString());
|
||||
int webserverPort;
|
||||
{
|
||||
bool ok{};
|
||||
webserverPort = settings.value("Webserver/port", 1234).toInt(&ok);
|
||||
if (!ok)
|
||||
qFatal("could not parse webserver port");
|
||||
}
|
||||
|
||||
EspRemoteAgent agent{std::move(serialPortConfigs)};
|
||||
if (!agent.listen(webserverListen, webserverPort))
|
||||
qFatal("could not start listening %s", qPrintable(agent.errorString()));
|
||||
|
||||
return app.exec();
|
||||
}
|
9
espremotemanager.pro
Normal file
9
espremotemanager.pro
Normal file
@ -0,0 +1,9 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS += \
|
||||
espremoteagent \
|
||||
espremotemanager \
|
||||
webserver
|
||||
|
||||
espremoteagent.depends += webserver
|
||||
espremotemanager.depends += webserver
|
119
espremotemanager/espremoteclient.cpp
Normal file
119
espremotemanager/espremoteclient.cpp
Normal file
@ -0,0 +1,119 @@
|
||||
#include "espremoteclient.h"
|
||||
|
||||
#include <QWebSocket>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
EspRemoteClient::EspRemoteClient(QWebSocket *websocket, EspRemoteManager &manager, QObject *parent) :
|
||||
QObject{parent},
|
||||
m_websocket{websocket},
|
||||
m_manager{manager}
|
||||
{
|
||||
qDebug() << "connected" << m_websocket->peerAddress().toString() << m_websocket->peerPort();
|
||||
|
||||
connect(m_websocket.get(), &QWebSocket::disconnected, this, &QObject::deleteLater);
|
||||
connect(m_websocket.get(), &QWebSocket::textMessageReceived, this, &EspRemoteClient::textMessageReceived);
|
||||
}
|
||||
|
||||
EspRemoteClient::~EspRemoteClient()
|
||||
{
|
||||
qDebug() << "disconnected" << m_websocket->peerAddress().toString() << m_websocket->peerPort();
|
||||
}
|
||||
|
||||
QString EspRemoteClient::peer() const
|
||||
{
|
||||
return QStringLiteral("%0:%1").arg(m_websocket->peerAddress().toString()).arg(m_websocket->peerPort());
|
||||
}
|
||||
|
||||
QString EspRemoteClient::path() const
|
||||
{
|
||||
return m_websocket->requestUrl().path();
|
||||
}
|
||||
|
||||
QString EspRemoteClient::logOutput() const
|
||||
{
|
||||
QString str;
|
||||
for (const auto &line : m_logOutput)
|
||||
{
|
||||
if (!str.isEmpty())
|
||||
str += "\n";
|
||||
str += line.toHtmlEscaped();
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
void EspRemoteClient::reboot()
|
||||
{
|
||||
m_websocket->sendTextMessage(QJsonDocument{QJsonObject{
|
||||
{"type", "reboot"}
|
||||
}}.toJson());
|
||||
}
|
||||
|
||||
void EspRemoteClient::textMessageReceived(const QString &message)
|
||||
{
|
||||
// qDebug() << message;
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "could not parse json command:" << error.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isObject())
|
||||
{
|
||||
qWarning() << "json command is not an object";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto obj = doc.object();
|
||||
|
||||
if (!obj.contains("type"))
|
||||
{
|
||||
qWarning() << "json command does not contain a type";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto typeVal = obj.value("type");
|
||||
|
||||
if (!typeVal.isString())
|
||||
{
|
||||
qWarning() << "json command type is not a string";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto type = typeVal.toString();
|
||||
|
||||
if (type == "log")
|
||||
{
|
||||
if (!obj.contains("line"))
|
||||
{
|
||||
qWarning() << "json command does not contain a line";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto lineVal = obj.value("line");
|
||||
|
||||
if (!lineVal.isString())
|
||||
{
|
||||
qWarning() << "json command line is not a string";
|
||||
return;
|
||||
}
|
||||
|
||||
auto line = lineVal.toString();
|
||||
|
||||
logReceived(std::move(line));
|
||||
}
|
||||
else
|
||||
qWarning() << "unknown command type" << type;
|
||||
}
|
||||
|
||||
void EspRemoteClient::logReceived(QString &&line)
|
||||
{
|
||||
m_logOutput.push(std::move(line));
|
||||
|
||||
while (m_logOutput.size() > 10)
|
||||
m_logOutput.pop();
|
||||
}
|
36
espremotemanager/espremoteclient.h
Normal file
36
espremotemanager/espremoteclient.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "webserverutils.h"
|
||||
|
||||
class QWebSocket;
|
||||
class EspRemoteManager;
|
||||
|
||||
class EspRemoteClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EspRemoteClient(QWebSocket *websocket, EspRemoteManager &manager, QObject *parent = nullptr);
|
||||
~EspRemoteClient() override;
|
||||
|
||||
QString peer() const;
|
||||
QString path() const;
|
||||
QString logOutput() const;
|
||||
|
||||
void reboot();
|
||||
|
||||
private slots:
|
||||
void textMessageReceived(const QString &message);
|
||||
|
||||
private:
|
||||
void logReceived(QString &&line);
|
||||
|
||||
const std::unique_ptr<QWebSocket> m_websocket;
|
||||
EspRemoteManager &m_manager;
|
||||
|
||||
iterable_queue<QString> m_logOutput;
|
||||
};
|
136
espremotemanager/espremotemanager.cpp
Normal file
136
espremotemanager/espremotemanager.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
#include "espremotemanager.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QWebSocketServer>
|
||||
|
||||
#include "webservercontainer.h"
|
||||
#include "webserverclientconnection.h"
|
||||
#include "espremoteclient.h"
|
||||
|
||||
EspRemoteManager::EspRemoteManager(QWebSocketServer &websocketServer, QObject *parent) :
|
||||
AbstractWebserver{parent},
|
||||
m_websocketServer{websocketServer}
|
||||
{
|
||||
connect(&m_websocketServer, &QWebSocketServer::newConnection, this, &EspRemoteManager::newConnection);
|
||||
}
|
||||
|
||||
EspRemoteManager::~EspRemoteManager() = default;
|
||||
|
||||
void EspRemoteManager::requestReceived(WebserverClientConnection &client, const Request &request)
|
||||
{
|
||||
QUrl url{request.path};
|
||||
QUrlQuery query{url};
|
||||
|
||||
if (url.path() == "/")
|
||||
{
|
||||
sendRootResponse(client, url, query);
|
||||
}
|
||||
else if (url.path() == "/reboot")
|
||||
{
|
||||
sendRebootResponse(client, url, query);
|
||||
}
|
||||
else
|
||||
if (!client.sendFullResponse(404, "Not Found", {{"Content-Type", "text/plain"}}, "The requested path \"" + request.path + "\" was not found."))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
||||
|
||||
void EspRemoteManager::newConnection()
|
||||
{
|
||||
while (const auto socket = m_websocketServer.nextPendingConnection())
|
||||
{
|
||||
auto ö = new EspRemoteClient(socket, *this, this);
|
||||
connect(ö, &QObject::destroyed, this, &EspRemoteManager::clientDestroyed);
|
||||
m_clients.emplace_back(ö);
|
||||
}
|
||||
}
|
||||
|
||||
void EspRemoteManager::clientDestroyed(QObject *object)
|
||||
{
|
||||
m_clients.erase(std::remove(std::begin(m_clients), std::end(m_clients), object), std::end(m_clients));
|
||||
}
|
||||
|
||||
void EspRemoteManager::sendRootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
QString content =
|
||||
"<!doctype html>"
|
||||
"<html lang=\"en\">"
|
||||
"<head>"
|
||||
"<meta charset=\"utf-8\" />"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />"
|
||||
"<title>ESP Remote Manager</title>"
|
||||
"<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT\" crossorigin=\"anonymous\" />"
|
||||
"<link rel=\"stylesheet\" href=\"https://cdn.datatables.net/1.12.1/css/dataTables.bootstrap5.min.css\" integrity=\"sha384-V05SibXwq2x9UKqEnsL0EnGlGPdbHwwdJdMjmp/lw3ruUri9L34ioOghMTZ8IHiI\" crossorigin=\"anonymous\">"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<h1>ESP Remote Manager</h1>"
|
||||
"<table class=\"table table-striped table-bordered table-sm\" style=\"width: initial;\">"
|
||||
"<thead>"
|
||||
"<tr>"
|
||||
"<th>Peer</th>"
|
||||
"<th>WS Path</th>"
|
||||
"<th>Actions</th>"
|
||||
"<th>Log</th>"
|
||||
"</tr>"
|
||||
"</thead>"
|
||||
"<tbody>";
|
||||
|
||||
for (auto port : m_clients)
|
||||
{
|
||||
content += QStringLiteral("<tr>"
|
||||
"<td>%0</td>"
|
||||
"<td>%1</td>"
|
||||
"<td><a href=\"reboot?peer=%0\">Reboot</a></td>"
|
||||
"<td><pre>%2</pre></td>"
|
||||
"</tr>")
|
||||
.arg(port->peer())
|
||||
.arg(port->path())
|
||||
.arg(port->logOutput());
|
||||
}
|
||||
|
||||
content += "</tbody>"
|
||||
"</table>"
|
||||
"<script src=\"https://code.jquery.com/jquery-3.6.1.min.js\" integrity=\"sha384-i61gTtaoovXtAbKjo903+O55Jkn2+RtzHtvNez+yI49HAASvznhe9sZyjaSHTau9\" crossorigin=\"anonymous\"></script>"
|
||||
"<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8\" crossorigin=\"anonymous\"></script>"
|
||||
"<script src=\"https://cdn.datatables.net/1.12.1/js/jquery.dataTables.min.js\" integrity=\"sha384-ZuLbSl+Zt/ry1/xGxjZPkp9P5MEDotJcsuoHT0cM8oWr+e1Ide//SZLebdVrzb2X\" crossorigin=\"anonymous\"></script>"
|
||||
"<script src=\"https://cdn.datatables.net/1.12.1/js/dataTables.bootstrap5.min.js\" integrity=\"sha384-jIAE3P7Re8BgMkT0XOtfQ6lzZgbDw/02WeRMJvXK3WMHBNynEx5xofqia1OHuGh0\" crossorigin=\"anonymous\"></script>"
|
||||
"<script>"
|
||||
"$(document).ready(function () {"
|
||||
"$('table').DataTable({"
|
||||
"filter: false,"
|
||||
"filtering: false,"
|
||||
"paging: false,"
|
||||
"info: false,"
|
||||
"});"
|
||||
"});"
|
||||
"</script>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/html"}}, content.toUtf8()))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
||||
|
||||
void EspRemoteManager::sendRebootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query)
|
||||
{
|
||||
if (!query.hasQueryItem("peer"))
|
||||
{
|
||||
if (!client.sendFullResponse(400, "Bad Request", {{"Content-Type", "text/plain"}}, "peer missing"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
const auto peer = query.queryItemValue("peer");
|
||||
auto iter = std::find_if(std::begin(m_clients), std::end(m_clients), [&peer](EspRemoteClient *client){ return client->peer() == peer; });
|
||||
if (iter == std::end(m_clients))
|
||||
{
|
||||
if (!client.sendFullResponse(404, "Bad Request", {{"Content-Type", "text/plain"}}, "peer not found"))
|
||||
qWarning() << "sending response failed";
|
||||
return;
|
||||
}
|
||||
|
||||
(*iter)->reboot();
|
||||
|
||||
if (!client.sendFullResponse(200, "Ok", {{"Content-Type", "text/plain"}}, "peer reboot command sent!"))
|
||||
qWarning() << "sending response failed";
|
||||
}
|
32
espremotemanager/espremotemanager.h
Normal file
32
espremotemanager/espremotemanager.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "abstractwebserver.h"
|
||||
|
||||
class QWebSocketServer;
|
||||
class EspRemoteClient;
|
||||
class QUrl;
|
||||
class QUrlQuery;
|
||||
|
||||
class EspRemoteManager : public AbstractWebserver
|
||||
{
|
||||
public:
|
||||
explicit EspRemoteManager(QWebSocketServer &websocketServer, QObject *parent = nullptr);
|
||||
~EspRemoteManager() override;
|
||||
|
||||
protected:
|
||||
void requestReceived(WebserverClientConnection &client, const Request &request) override;
|
||||
|
||||
private slots:
|
||||
void newConnection();
|
||||
void clientDestroyed(QObject *object);
|
||||
|
||||
private:
|
||||
void sendRootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
void sendRebootResponse(WebserverClientConnection &client, const QUrl &url, const QUrlQuery &query);
|
||||
|
||||
QWebSocketServer &m_websocketServer;
|
||||
|
||||
std::vector<EspRemoteClient*> m_clients;
|
||||
};
|
7
espremotemanager/espremotemanager.ini
Normal file
7
espremotemanager/espremotemanager.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[Webserver]
|
||||
listen=Any
|
||||
port=80
|
||||
|
||||
[Websocket]
|
||||
listen=Any
|
||||
port=81
|
30
espremotemanager/espremotemanager.pro
Normal file
30
espremotemanager/espremotemanager.pro
Normal file
@ -0,0 +1,30 @@
|
||||
QT = core network websockets
|
||||
|
||||
TARGET = espremotemanager
|
||||
|
||||
TEMPLATE = app
|
||||
|
||||
CONFIG += console
|
||||
CONFIG -= app_bundle
|
||||
|
||||
PROJECT_ROOT = ..
|
||||
|
||||
DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/bin
|
||||
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
DBLIBS += webserver
|
||||
|
||||
HEADERS += \
|
||||
espremoteclient.h \
|
||||
espremotemanager.h
|
||||
|
||||
SOURCES += \
|
||||
espremoteclient.cpp \
|
||||
espremotemanager.cpp \
|
||||
main.cpp
|
||||
|
||||
OTHER_FILES += \
|
||||
espremotemanager.ini
|
||||
|
||||
include($${PROJECT_ROOT}/project.pri)
|
54
espremotemanager/main.cpp
Normal file
54
espremotemanager/main.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QSettings>
|
||||
#include <QWebSocketServer>
|
||||
|
||||
#include "webserverutils.h"
|
||||
#include "espremotemanager.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} "
|
||||
"["
|
||||
"%{if-debug}D%{endif}"
|
||||
"%{if-info}I%{endif}"
|
||||
"%{if-warning}W%{endif}"
|
||||
"%{if-critical}C%{endif}"
|
||||
"%{if-fatal}F%{endif}"
|
||||
"] "
|
||||
"%{function}(): "
|
||||
"%{message}"));
|
||||
|
||||
QCoreApplication app{argc, argv};
|
||||
|
||||
QSettings settings{"espremotemanager.ini", QSettings::IniFormat};
|
||||
|
||||
QHostAddress webserverListen = parseHostAddress(settings.value("Webserver/listen").toString());
|
||||
int webserverPort;
|
||||
{
|
||||
bool ok{};
|
||||
webserverPort = settings.value("Webserver/port", 1234).toInt(&ok);
|
||||
if (!ok)
|
||||
qFatal("could not parse webserver port");
|
||||
}
|
||||
|
||||
QHostAddress websocketListen = parseHostAddress(settings.value("Websocket/listen").toString());
|
||||
int websocketPort;
|
||||
{
|
||||
bool ok{};
|
||||
websocketPort = settings.value("Websocket/port", 1234).toInt(&ok);
|
||||
if (!ok)
|
||||
qFatal("could not parse webserver port");
|
||||
}
|
||||
|
||||
QWebSocketServer websocketServer{"dafuq", QWebSocketServer::NonSecureMode};
|
||||
|
||||
EspRemoteManager manager{websocketServer};
|
||||
|
||||
if (!manager.listen(webserverListen, webserverPort))
|
||||
qFatal("could not start webserver listening %s", qPrintable(manager.errorString()));
|
||||
|
||||
if (!websocketServer.listen(websocketListen, websocketPort))
|
||||
qFatal("could not start webserver listening %s", qPrintable(manager.errorString()));
|
||||
|
||||
return app.exec();
|
||||
}
|
27
project.pri
Normal file
27
project.pri
Normal file
@ -0,0 +1,27 @@
|
||||
CONFIG += c++17
|
||||
|
||||
DEFINES += QT_DEPRECATED_WARNINGS \
|
||||
QT_DISABLE_DEPRECATED_BEFORE=0x060000 \
|
||||
QT_MESSAGELOGCONTEXT
|
||||
|
||||
equals(TEMPLATE, "lib") {
|
||||
win32: DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/bin
|
||||
else: DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/lib
|
||||
}
|
||||
|
||||
!isEmpty(DBLIBS) {
|
||||
win32: LIBS += -L$${OUT_PWD}/$${PROJECT_ROOT}/bin
|
||||
else: LIBS += -Wl,-rpath=\\\$$ORIGIN/../lib -L$${OUT_PWD}/$${PROJECT_ROOT}/lib
|
||||
}
|
||||
|
||||
contains(DBLIBS, webserver) {
|
||||
LIBS += -lwebserver
|
||||
|
||||
INCLUDEPATH += $$PWD/webserver
|
||||
DEPENDPATH += $$PWD/webserver
|
||||
}
|
||||
|
||||
isEmpty(QMAKE_LRELEASE) {
|
||||
win32:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]\lrelease.exe
|
||||
else:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease
|
||||
}
|
101
webserver/abstractwebserver.cpp
Normal file
101
webserver/abstractwebserver.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
#include "abstractwebserver.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
#include <QNetworkProxy>
|
||||
#endif
|
||||
|
||||
#include "webserverclientconnection.h"
|
||||
|
||||
AbstractWebserver::AbstractWebserver(QObject *parent) :
|
||||
QObject{parent},
|
||||
m_server{std::make_unique<QTcpServer>(this)}
|
||||
{
|
||||
connect(m_server.get(), &QTcpServer::newConnection, this, &AbstractWebserver::newConnection);
|
||||
connect(m_server.get(), &QTcpServer::acceptError, this, &AbstractWebserver::acceptError);
|
||||
}
|
||||
|
||||
AbstractWebserver::~AbstractWebserver() = default;
|
||||
|
||||
bool AbstractWebserver::listen(const QHostAddress &address, quint16 port)
|
||||
{
|
||||
return m_server->listen(address, port);
|
||||
}
|
||||
|
||||
void AbstractWebserver::close()
|
||||
{
|
||||
m_server->close();
|
||||
}
|
||||
|
||||
bool AbstractWebserver::isListening() const
|
||||
{
|
||||
return m_server->isListening();
|
||||
}
|
||||
|
||||
void AbstractWebserver::setMaxPendingConnections(int numConnections)
|
||||
{
|
||||
m_server->setMaxPendingConnections(numConnections);
|
||||
}
|
||||
|
||||
int AbstractWebserver::maxPendingConnections() const
|
||||
{
|
||||
return m_server->maxPendingConnections();
|
||||
}
|
||||
|
||||
quint16 AbstractWebserver::serverPort() const
|
||||
{
|
||||
return m_server->serverPort();
|
||||
}
|
||||
|
||||
QHostAddress AbstractWebserver::serverAddress() const
|
||||
{
|
||||
return m_server->serverAddress();
|
||||
}
|
||||
|
||||
qintptr AbstractWebserver::socketDescriptor() const
|
||||
{
|
||||
return m_server->socketDescriptor();
|
||||
}
|
||||
|
||||
bool AbstractWebserver::setSocketDescriptor(qintptr socketDescriptor)
|
||||
{
|
||||
return m_server->setSocketDescriptor(socketDescriptor);
|
||||
}
|
||||
|
||||
QAbstractSocket::SocketError AbstractWebserver::serverError() const
|
||||
{
|
||||
return m_server->serverError();
|
||||
}
|
||||
|
||||
QString AbstractWebserver::errorString() const
|
||||
{
|
||||
return m_server->errorString();
|
||||
}
|
||||
|
||||
void AbstractWebserver::pauseAccepting()
|
||||
{
|
||||
m_server->pauseAccepting();
|
||||
}
|
||||
|
||||
void AbstractWebserver::resumeAccepting()
|
||||
{
|
||||
m_server->resumeAccepting();
|
||||
}
|
||||
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
void AbstractWebserver::setProxy(const QNetworkProxy &networkProxy)
|
||||
{
|
||||
m_server->setProxy(networkProxy);
|
||||
}
|
||||
|
||||
QNetworkProxy AbstractWebserver::proxy() const
|
||||
{
|
||||
return m_server->proxy();
|
||||
}
|
||||
#endif
|
||||
|
||||
void AbstractWebserver::newConnection()
|
||||
{
|
||||
while (const auto socket = m_server->nextPendingConnection())
|
||||
new WebserverClientConnection{*socket, *this, this};
|
||||
}
|
62
webserver/abstractwebserver.h
Normal file
62
webserver/abstractwebserver.h
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractSocket>
|
||||
#include <QHostAddress>
|
||||
|
||||
#include "webserver_global.h"
|
||||
|
||||
class QTcpServer;
|
||||
|
||||
class WebserverClientConnection;
|
||||
class Request;
|
||||
|
||||
class WEBSERVER_EXPORT AbstractWebserver : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(AbstractWebserver)
|
||||
|
||||
public:
|
||||
explicit AbstractWebserver(QObject *parent = nullptr);
|
||||
~AbstractWebserver() override;
|
||||
|
||||
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
|
||||
void close();
|
||||
|
||||
bool isListening() const;
|
||||
|
||||
void setMaxPendingConnections(int numConnections);
|
||||
int maxPendingConnections() const;
|
||||
|
||||
quint16 serverPort() const;
|
||||
QHostAddress serverAddress() const;
|
||||
|
||||
qintptr socketDescriptor() const;
|
||||
bool setSocketDescriptor(qintptr socketDescriptor);
|
||||
|
||||
QAbstractSocket::SocketError serverError() const;
|
||||
QString errorString() const;
|
||||
|
||||
void pauseAccepting();
|
||||
void resumeAccepting();
|
||||
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
void setProxy(const QNetworkProxy &networkProxy);
|
||||
QNetworkProxy proxy() const;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend class WebserverClientConnection;
|
||||
virtual void requestReceived(WebserverClientConnection &client, const Request &request) = 0;
|
||||
|
||||
Q_SIGNALS:
|
||||
void acceptError(QAbstractSocket::SocketError socketError);
|
||||
|
||||
private slots:
|
||||
void newConnection();
|
||||
|
||||
private:
|
||||
const std::unique_ptr<QTcpServer> m_server;
|
||||
};
|
23
webserver/webserver.pro
Normal file
23
webserver/webserver.pro
Normal file
@ -0,0 +1,23 @@
|
||||
QT += core network
|
||||
QT -= gui widgets
|
||||
|
||||
TARGET = webserver
|
||||
TEMPLATE = lib
|
||||
|
||||
PROJECT_ROOT = ..
|
||||
|
||||
DEFINES += WEBSERVER_LIBRARY
|
||||
|
||||
HEADERS += \
|
||||
abstractwebserver.h \
|
||||
webservercontainer.h \
|
||||
webserverclientconnection.h \
|
||||
webserver_global.h \
|
||||
webserverutils.h
|
||||
|
||||
SOURCES += \
|
||||
abstractwebserver.cpp \
|
||||
webserverclientconnection.cpp \
|
||||
webserverutils.cpp
|
||||
|
||||
include($${PROJECT_ROOT}/project.pri)
|
9
webserver/webserver_global.h
Normal file
9
webserver/webserver_global.h
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if defined(WEBSERVER_LIBRARY)
|
||||
# define WEBSERVER_EXPORT Q_DECL_EXPORT
|
||||
#else
|
||||
# define WEBSERVER_EXPORT Q_DECL_IMPORT
|
||||
#endif
|
143
webserver/webserverclientconnection.cpp
Normal file
143
webserver/webserverclientconnection.cpp
Normal file
@ -0,0 +1,143 @@
|
||||
#include "webserverclientconnection.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QTcpSocket>
|
||||
|
||||
#include "abstractwebserver.h"
|
||||
|
||||
WebserverClientConnection::WebserverClientConnection(QTcpSocket &socket, AbstractWebserver &webserver, QObject *parent) :
|
||||
QObject{parent},
|
||||
m_socket{&socket},
|
||||
m_webserver{webserver}
|
||||
{
|
||||
// qDebug() << "connected";
|
||||
m_socket->setParent(this);
|
||||
|
||||
connect(m_socket.get(), &QTcpSocket::readyRead, this, &WebserverClientConnection::readyRead);
|
||||
connect(m_socket.get(), &QTcpSocket::disconnected, this, &QObject::deleteLater);
|
||||
}
|
||||
|
||||
WebserverClientConnection::~WebserverClientConnection()
|
||||
{
|
||||
// qDebug() << "disconnected";
|
||||
}
|
||||
|
||||
bool WebserverClientConnection::sendResponseHeaders(int status, const QByteArray &message, const QMap<QByteArray, QByteArray> &responseHeaders)
|
||||
{
|
||||
if (m_status != Response)
|
||||
return false;
|
||||
|
||||
if (!writeLine(m_request.protocol + ' ' + QString::number(status).toUtf8() + ' ' + message))
|
||||
return false;
|
||||
|
||||
for (auto iter = std::begin(responseHeaders); iter != std::end(responseHeaders); iter++)
|
||||
if (!writeLine(iter.key() + ": " + iter.value()))
|
||||
return false;
|
||||
|
||||
if (!writeLine({}))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebserverClientConnection::sendFullResponse(int status, const QByteArray &message,
|
||||
QMap<QByteArray, QByteArray> responseHeaders, const QByteArray &response)
|
||||
{
|
||||
if (m_status != Response)
|
||||
{
|
||||
qWarning() << "status not response";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto containsKey = [&](auto key){
|
||||
for (auto iter = std::begin(responseHeaders); iter != std::end(responseHeaders); iter++)
|
||||
if (iter.key().compare(key, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!containsKey("Connection"))
|
||||
responseHeaders.insert("Connection", "keep");
|
||||
|
||||
if (!response.isEmpty() && !containsKey("Content-Length"))
|
||||
responseHeaders.insert("Content-Length", QString::number(response.size()).toUtf8());
|
||||
|
||||
if (!sendResponseHeaders(status, message, responseHeaders))
|
||||
return false;
|
||||
|
||||
m_socket->write(response);
|
||||
m_socket->flush();
|
||||
|
||||
m_request.clear();
|
||||
m_status = RequestLine;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebserverClientConnection::readyRead()
|
||||
{
|
||||
while (m_socket->canReadLine())
|
||||
{
|
||||
auto line = m_socket->readLine();
|
||||
if (line.endsWith('\n'))
|
||||
{
|
||||
line.chop(1);
|
||||
if (line.endsWith('\r'))
|
||||
line.chop(1);
|
||||
}
|
||||
|
||||
// qDebug() << line;
|
||||
|
||||
switch (m_status)
|
||||
{
|
||||
case RequestLine:
|
||||
{
|
||||
auto parts = line.split(' ');
|
||||
if (parts.size() < 3)
|
||||
{
|
||||
qWarning() << "invalid request line" << line;
|
||||
m_socket->close();
|
||||
return;
|
||||
}
|
||||
|
||||
m_request.method = parts.takeFirst();
|
||||
m_request.path = parts.takeFirst();
|
||||
m_request.protocol = parts.join(' ');
|
||||
|
||||
m_status = RequestHeaders;
|
||||
continue;
|
||||
}
|
||||
case RequestHeaders:
|
||||
{
|
||||
if (line.isEmpty())
|
||||
{
|
||||
m_status = Response;
|
||||
|
||||
m_webserver.requestReceived(*this, m_request);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto index = line.indexOf(": ");
|
||||
if (index == -1)
|
||||
qWarning() << "could not parse request header" << line;
|
||||
else
|
||||
m_request.headers.insert(line.left(index), line.mid(index + 2));
|
||||
}
|
||||
|
||||
continue;
|
||||
default:
|
||||
qWarning() << "received data in unexpected state" << m_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WebserverClientConnection::writeLine(const QByteArray &buf)
|
||||
{
|
||||
// qDebug() << buf;
|
||||
m_socket->write(buf);
|
||||
m_socket->write("\r\n");
|
||||
|
||||
return true;
|
||||
}
|
42
webserver/webserverclientconnection.h
Normal file
42
webserver/webserverclientconnection.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
|
||||
#include "webserver_global.h"
|
||||
#include "webservercontainer.h"
|
||||
|
||||
class QTcpSocket;
|
||||
|
||||
class AbstractWebserver;
|
||||
|
||||
class WEBSERVER_EXPORT WebserverClientConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WebserverClientConnection(QTcpSocket &socket, AbstractWebserver &webserver, QObject *parent = nullptr);
|
||||
~WebserverClientConnection() override;
|
||||
|
||||
bool sendResponseHeaders(int status, const QByteArray &message,
|
||||
const QMap<QByteArray, QByteArray> &responseHeaders);
|
||||
bool sendFullResponse(int status, const QByteArray &message,
|
||||
QMap<QByteArray, QByteArray> responseHeaders, const QByteArray &response);
|
||||
|
||||
private slots:
|
||||
void readyRead();
|
||||
|
||||
private:
|
||||
bool writeLine(const QByteArray &buf);
|
||||
|
||||
const std::unique_ptr<QTcpSocket> m_socket;
|
||||
AbstractWebserver &m_webserver;
|
||||
|
||||
enum Status {
|
||||
RequestLine, RequestHeaders, Response
|
||||
};
|
||||
Status m_status{RequestLine};
|
||||
|
||||
Request m_request;
|
||||
};
|
22
webserver/webservercontainer.h
Normal file
22
webserver/webservercontainer.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QMap>
|
||||
|
||||
#include "webserver_global.h"
|
||||
|
||||
struct WEBSERVER_EXPORT Request
|
||||
{
|
||||
QByteArray method;
|
||||
QByteArray path;
|
||||
QByteArray protocol;
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
|
||||
void clear()
|
||||
{
|
||||
method.clear();
|
||||
path.clear();
|
||||
protocol.clear();
|
||||
headers.clear();
|
||||
}
|
||||
};
|
17
webserver/webserverutils.cpp
Normal file
17
webserver/webserverutils.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include "webserverutils.h"
|
||||
|
||||
QHostAddress parseHostAddress(const QString &str)
|
||||
{
|
||||
if (str.isEmpty() || str == "Any")
|
||||
return QHostAddress::Any;
|
||||
else if (str == "AnyIPv6")
|
||||
return QHostAddress::AnyIPv6;
|
||||
else if (str == "AnyIPv4")
|
||||
return QHostAddress::AnyIPv4;
|
||||
else if (str == "LocalHost")
|
||||
return QHostAddress::LocalHost;
|
||||
else if (str == "LocalHostIPv6")
|
||||
return QHostAddress::LocalHostIPv6;
|
||||
else
|
||||
return QHostAddress{str};
|
||||
}
|
22
webserver/webserverutils.h
Normal file
22
webserver/webserverutils.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <queue>
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
#include "webserver_global.h"
|
||||
|
||||
QHostAddress WEBSERVER_EXPORT parseHostAddress(const QString &str);
|
||||
|
||||
template<typename T, typename Container=std::deque<T> >
|
||||
class WEBSERVER_EXPORT iterable_queue : public std::queue<T,Container>
|
||||
{
|
||||
public:
|
||||
typedef typename Container::iterator iterator;
|
||||
typedef typename Container::const_iterator const_iterator;
|
||||
|
||||
iterator begin() { return this->c.begin(); }
|
||||
iterator end() { return this->c.end(); }
|
||||
const_iterator begin() const { return this->c.begin(); }
|
||||
const_iterator end() const { return this->c.end(); }
|
||||
};
|
Reference in New Issue
Block a user