From 274668ed60ffb4ede18c106fc31ce1dd389c43f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 5 Feb 2021 17:08:26 +0100 Subject: [PATCH] Add nymea light classes --- ws2812fx/nymealight.cpp | 92 +++++++++++++++++ ws2812fx/nymealight.h | 41 ++++++++ ws2812fx/nymealightinterface.cpp | 6 ++ ws2812fx/nymealightinterface.h | 76 ++++++++++++++ ws2812fx/nymealightserialinterface.cpp | 133 +++++++++++++++++++++++++ ws2812fx/nymealightserialinterface.h | 39 ++++++++ 6 files changed, 387 insertions(+) create mode 100644 ws2812fx/nymealight.cpp create mode 100644 ws2812fx/nymealight.h create mode 100644 ws2812fx/nymealightinterface.cpp create mode 100644 ws2812fx/nymealightinterface.h create mode 100644 ws2812fx/nymealightserialinterface.cpp create mode 100644 ws2812fx/nymealightserialinterface.h diff --git a/ws2812fx/nymealight.cpp b/ws2812fx/nymealight.cpp new file mode 100644 index 00000000..7ccce8e1 --- /dev/null +++ b/ws2812fx/nymealight.cpp @@ -0,0 +1,92 @@ +#include "nymealight.h" +#include "extern-plugininfo.h" + +#include + +NymeaLight::NymeaLight(NymeaLightInterface *interface, QObject *parent) : + QObject(parent), + m_interface(interface) +{ + connect(m_interface, &NymeaLightInterface::availableChanged, this, [=](bool available){ + qCDebug(dcWs2812fx()) << "Interface available changed" << available; + emit availableChanged(available); + }); + + connect(m_interface, &NymeaLightInterface::dataReceived, this, &NymeaLight::onDataReceived); +} + +NymeaLightInterfaceReply *NymeaLight::setColor(const QColor &color, quint16 fadeDuration) +{ + // Build the request + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(NymeaLightInterface::CommandSetColor); + stream << m_requestId++; + if (fadeDuration > 0) { + stream << static_cast(NymeaLightInterface::ModeFade); + } + stream << static_cast(color.red()); + stream << static_cast(color.green()); + stream << static_cast(color.blue()); + if (fadeDuration > 0) { + stream << fadeDuration; + } + + NymeaLightInterfaceReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + + return reply; +} + +bool NymeaLight::available() const +{ + return m_interface->available(); +} + +NymeaLightInterfaceReply *NymeaLight::createReply(const QByteArray &requestData) +{ + NymeaLightInterfaceReply *reply = new NymeaLightInterfaceReply(requestData, this); + connect(reply, &NymeaLightInterfaceReply::finished, reply, &NymeaLightInterfaceReply::deleteLater); + return reply; +} + +void NymeaLight::sendNextRequest() +{ + if (m_currentReply) + return; + + if (m_pendingRequests.isEmpty()) + return; + + // TODO: if not available, finish all replies with unknown error + + m_currentReply = m_pendingRequests.dequeue(); + qCDebug(dcWs2812fx()) << "Sending request" << m_currentReply->command() << m_currentReply->requestId() << m_currentReply->requestData().toHex(); + m_interface->sendData(m_currentReply->requestData()); +} + +void NymeaLight::onDataReceived(const QByteArray &data) +{ + qCDebug(dcWs2812fx()) << "Recived data" << data; + Q_ASSERT(data.length() >= 3); + + NymeaLightInterface::Command command = static_cast(data.at(0)); + quint8 requestId = static_cast(data.at(1)); + NymeaLightInterface::Status status = static_cast(data.at(2)); + + if (m_currentReply) { + if (m_currentReply->command() == command && m_currentReply->requestId() == requestId) { + m_currentReply->m_status = status; + qCDebug(dcWs2812fx()) << "Request finished" << command << m_currentReply->requestId() << status; + emit m_currentReply->finished(); + + m_currentReply = nullptr; + sendNextRequest(); + } + } else { + qCWarning(dcWs2812fx()) << "Received unhandled data" << data.toHex(); + } + +} + diff --git a/ws2812fx/nymealight.h b/ws2812fx/nymealight.h new file mode 100644 index 00000000..02e154ee --- /dev/null +++ b/ws2812fx/nymealight.h @@ -0,0 +1,41 @@ +#ifndef NYMEALIGHT_H +#define NYMEALIGHT_H + +#include +#include +#include + +#include "nymealightinterface.h" + +class NymeaLight : public QObject +{ + Q_OBJECT +public: + explicit NymeaLight(NymeaLightInterface *interface, QObject *parent = nullptr); + + // Set the color. If fade duration is 0, the color will be set immediatly, + // otherwise it will fade to the color with the given fade duration + NymeaLightInterfaceReply *setColor(const QColor &color, quint16 fadeDuration = 0); + + bool available() const; + +private: + NymeaLightInterface *m_interface = nullptr; + quint8 m_requestId = 0; + + NymeaLightInterfaceReply *m_currentReply = nullptr; + QQueue m_pendingRequests; + + NymeaLightInterfaceReply *createReply(const QByteArray &requestData); + void sendNextRequest(); + +private slots: + void onDataReceived(const QByteArray &data); + +signals: + void availableChanged(bool available); + +}; + + +#endif // NYMEALIGHT_H diff --git a/ws2812fx/nymealightinterface.cpp b/ws2812fx/nymealightinterface.cpp new file mode 100644 index 00000000..e80cfc33 --- /dev/null +++ b/ws2812fx/nymealightinterface.cpp @@ -0,0 +1,6 @@ +#include "nymealightinterface.h" + +NymeaLightInterface::NymeaLightInterface(QObject *parent) : QObject(parent) +{ + +} diff --git a/ws2812fx/nymealightinterface.h b/ws2812fx/nymealightinterface.h new file mode 100644 index 00000000..74c215b3 --- /dev/null +++ b/ws2812fx/nymealightinterface.h @@ -0,0 +1,76 @@ +#ifndef NYMEALIGHTINTERFACE_H +#define NYMEALIGHTINTERFACE_H + +#include + +class NymeaLightInterface : public QObject +{ + Q_OBJECT +public: + enum Command { + CommandSetColor = 0x00, + CommandSetBrightness = 0x01, + CommandSetSpeed = 0x02, + CommandSetEffect = 0x03, + CommandCustom = 0xFF + }; + Q_ENUM(Command) + + enum Status { + StatusSuccess = 0x00, + StatusInvalidProtocol = 0x01, + StatusInvalidCommand = 0x02, + StatusInvalidPlayload = 0x03, + StatusUnknownError = 0xff + }; + Q_ENUM(Status) + + enum Mode { + ModeDirect = 0x00, + ModeFade = 0x01 + }; + Q_ENUM(Mode) + + explicit NymeaLightInterface(QObject *parent = nullptr); + + virtual bool open() = 0; + virtual void close() = 0; + virtual bool available() = 0; + + virtual void sendData(const QByteArray &data) = 0; + +signals: + void availableChanged(bool available); + void dataReceived(const QByteArray &data); + +}; + +class NymeaLightInterfaceReply : public QObject +{ + Q_OBJECT + + friend class NymeaLight; + +public: + QByteArray requestData() const { return m_requestData; }; + NymeaLightInterface::Command command() const { return m_command; }; + quint8 requestId() const { return m_requestId; }; + NymeaLightInterface::Status status() const { return m_status; }; + +signals: + void finished(); + +private: + explicit NymeaLightInterfaceReply(const QByteArray &requestData, QObject *parent = nullptr) : QObject(parent), m_requestData(requestData) { + Q_ASSERT(m_requestData.length() >= 2); + m_command = static_cast(m_requestData.at(0)); + m_requestId = static_cast(m_requestData.at(1)); + } + + QByteArray m_requestData; + NymeaLightInterface::Command m_command; + quint8 m_requestId; + NymeaLightInterface::Status m_status = NymeaLightInterface::StatusUnknownError; +}; + +#endif // NYMEALIGHTINTERFACE_H diff --git a/ws2812fx/nymealightserialinterface.cpp b/ws2812fx/nymealightserialinterface.cpp new file mode 100644 index 00000000..ad6126ec --- /dev/null +++ b/ws2812fx/nymealightserialinterface.cpp @@ -0,0 +1,133 @@ +#include "nymealightserialinterface.h" +#include "extern-plugininfo.h" + +#include + +NymeaLightSerialInterface::NymeaLightSerialInterface(const QString &name, QObject *parent) : + NymeaLightInterface(parent) +{ + m_serialPort = new QSerialPort(name, this); + m_serialPort->setBaudRate(115200); + m_serialPort->setDataBits(QSerialPort::DataBits::Data8); + m_serialPort->setParity(QSerialPort::Parity::NoParity); + m_serialPort->setStopBits(QSerialPort::StopBits::OneStop); + m_serialPort->setFlowControl(QSerialPort::FlowControl::NoFlowControl); + + connect(m_serialPort, &QSerialPort::readyRead, this, &NymeaLightSerialInterface::onReadyRead); + connect(m_serialPort, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(onSerialError(QSerialPort::SerialPortError))); + + m_reconnectTimer = new QTimer(this); + m_reconnectTimer->setInterval(5000); + m_reconnectTimer->setSingleShot(false); + connect(m_reconnectTimer, &QTimer::timeout, this, [=](){ + if (m_serialPort->isOpen()) { + m_reconnectTimer->stop(); + return; + } else { + if (open()) { + m_reconnectTimer->stop(); + } + } + }); +} + +bool NymeaLightSerialInterface::open() +{ + if (!m_serialPort->open(QIODevice::ReadWrite)) { + qCWarning(dcWs2812fx()) << "Could not open serial port" << m_serialPort->portName() << m_serialPort->errorString(); + return false; + } + + emit availableChanged(true); + return true; +} + +void NymeaLightSerialInterface::close() +{ + m_serialPort->close(); + emit availableChanged(false); +} + +bool NymeaLightSerialInterface::available() +{ + return m_serialPort->isOpen(); +} + +void NymeaLightSerialInterface::sendData(const QByteArray &data) +{ + // Stream bytes using SLIP + qCDebug(dcWs2812fx()) << "Write data" << data.toHex(); + + QByteArray message; + QDataStream stream(&message, QIODevice::WriteOnly); + for (int i = 0; i < data.length(); i++) { + quint8 dataByte = data.at(i); + switch (dataByte) { + case SlipProtocolEnd: + stream << static_cast(SlipProtocolEsc); + stream << static_cast(SlipProtocolTransposedEnd); + break; + case SlipProtocolEsc: + stream << static_cast(SlipProtocolEsc); + stream << static_cast(SlipProtocolTransposedEsc); + break; + default: + stream << dataByte; + break; + } + } + m_serialPort->write(message); + m_serialPort->flush(); +} + +void NymeaLightSerialInterface::onReadyRead() +{ + QByteArray data = m_serialPort->readAll(); + qCDebug(dcWs2812fx()) << "Received data" << data.toHex(); + for (int i = 0; i < data.length(); i++) { + quint8 receivedByte = data.at(i); + + if (m_protocolEscaping) { + switch (receivedByte) { + case SlipProtocolTransposedEnd: + m_buffer.append(static_cast(SlipProtocolEnd)); + m_protocolEscaping = false; + break; + case SlipProtocolTransposedEsc: + m_buffer.append(static_cast(SlipProtocolEsc)); + m_protocolEscaping = false; + break; + default: + // SLIP protocol violation...received escape, but it is not an escaped byte + break; + } + } + + switch (receivedByte) { + case SlipProtocolEnd: + // We are done with this package, process it and reset the buffer + emit dataReceived(m_buffer); + m_buffer.clear(); + m_protocolEscaping = false; + break; + case SlipProtocolEsc: + // The next byte will be escaped, lets wait for it + m_protocolEscaping = true; + break; + default: + // Nothing special, just add to buffer + m_buffer.append(receivedByte); + break; + } + } +} + +void NymeaLightSerialInterface::onSerialError(QSerialPort::SerialPortError error) +{ + if (error != QSerialPort::NoError && m_serialPort->isOpen()) { + qCCritical(dcWs2812fx()) << "Serial port error:" << error << m_serialPort->errorString(); + m_reconnectTimer->start(); + m_serialPort->close(); + emit availableChanged(false); + } +} diff --git a/ws2812fx/nymealightserialinterface.h b/ws2812fx/nymealightserialinterface.h new file mode 100644 index 00000000..d7fe3421 --- /dev/null +++ b/ws2812fx/nymealightserialinterface.h @@ -0,0 +1,39 @@ +#ifndef NYMEALIGHTSERIALINTERFACE_H +#define NYMEALIGHTSERIALINTERFACE_H + +#include +#include +#include + +#include "nymealightinterface.h" + +class NymeaLightSerialInterface : public NymeaLightInterface +{ + Q_OBJECT +public: + explicit NymeaLightSerialInterface(const QString &name, QObject *parent = nullptr); + + bool open() override; + void close() override; + bool available() override; + void sendData(const QByteArray &data) override; + +private: + enum SlipProtocol { + SlipProtocolEnd = 0xC0, + SlipProtocolEsc = 0xDB, + SlipProtocolTransposedEnd = 0xDC, + SlipProtocolTransposedEsc = 0xDD + }; + + QTimer *m_reconnectTimer = nullptr; + QSerialPort *m_serialPort = nullptr; + QByteArray m_buffer; + bool m_protocolEscaping = false; + +private slots: + void onReadyRead(); + void onSerialError(QSerialPort::SerialPortError error); +}; + +#endif // NYMEALIGHTSERIALINTERFACE_H