Import existing sources

This commit is contained in:
2022-09-25 01:31:36 +02:00
parent 38704e2f03
commit 68a17e2442
26 changed files with 1660 additions and 0 deletions

View 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;
}
}

View 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;
};

View 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

View 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)

View File

@ -0,0 +1,11 @@
#pragma once
#include <QString>
#include <QUrl>
struct SerialPortConfig
{
QString port;
int baudrate;
QUrl url;
};

View 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;
}

View 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
View 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
View File

@ -0,0 +1,9 @@
TEMPLATE = subdirs
SUBDIRS += \
espremoteagent \
espremotemanager \
webserver
espremoteagent.depends += webserver
espremotemanager.depends += webserver

View 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();
}

View 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;
};

View 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";
}

View 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;
};

View File

@ -0,0 +1,7 @@
[Webserver]
listen=Any
port=80
[Websocket]
listen=Any
port=81

View 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
View 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
View 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
}

View 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};
}

View 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
View 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)

View 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

View 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;
}

View 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;
};

View 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();
}
};

View 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};
}

View 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(); }
};