diff --git a/ws2812fx/firmware/include/README b/ws2812fx/firmware/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/ws2812fx/firmware/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/ws2812fx/firmware/lib/Adafruit_NeoPixel b/ws2812fx/firmware/lib/Adafruit_NeoPixel new file mode 160000 index 00000000..8595ee3f --- /dev/null +++ b/ws2812fx/firmware/lib/Adafruit_NeoPixel @@ -0,0 +1 @@ +Subproject commit 8595ee3f5880d5096d71551c621d7a2d3818f974 diff --git a/ws2812fx/firmware/lib/README b/ws2812fx/firmware/lib/README new file mode 100644 index 00000000..6debab1e --- /dev/null +++ b/ws2812fx/firmware/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/ws2812fx/firmware/lib/WS2812FX b/ws2812fx/firmware/lib/WS2812FX new file mode 160000 index 00000000..4774866a --- /dev/null +++ b/ws2812fx/firmware/lib/WS2812FX @@ -0,0 +1 @@ +Subproject commit 4774866a176eb4ff474f712ce63a06404385e911 diff --git a/ws2812fx/firmware/platformio.ini b/ws2812fx/firmware/platformio.ini new file mode 100644 index 00000000..ea23b772 --- /dev/null +++ b/ws2812fx/firmware/platformio.ini @@ -0,0 +1,14 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:uno] +platform = atmelavr +board = uno +framework = arduino diff --git a/ws2812fx/firmware/src/main.cpp b/ws2812fx/firmware/src/main.cpp new file mode 100644 index 00000000..928708c8 --- /dev/null +++ b/ws2812fx/firmware/src/main.cpp @@ -0,0 +1,20 @@ +#include + +#include "nymealight.h" + +// TEST +NymeaLight *light = nullptr; + +void setup() { + // Get rid of unused warning from lib + (void)&_modes; + + light = new NymeaLight(Serial, new WS2812FX(10, 13, NEO_GRB + NEO_KHZ800)); + light->init(); +} + +void loop() { + light->process(); +} + + diff --git a/ws2812fx/firmware/src/nymealight.cpp b/ws2812fx/firmware/src/nymealight.cpp new file mode 100644 index 00000000..ea2b40bf --- /dev/null +++ b/ws2812fx/firmware/src/nymealight.cpp @@ -0,0 +1,330 @@ +#include "nymealight.h" + +NymeaLight::NymeaLight(WS2812FX *strip) : + m_strip(strip) +{ + +} + + +NymeaLight::NymeaLight(SoftwareSerial *serial, WS2812FX *strip) : + m_softwareSerial(serial), + m_strip(strip) +{ + +} + +NymeaLight::NymeaLight(HardwareSerial &serial, WS2812FX *strip) : + m_hardwareSerial(&serial), + m_strip(strip) +{ + +} + +WS2812FX *NymeaLight::strip() const +{ + return m_strip; +} + +void NymeaLight::init() +{ + // Get rid of unused warning from lib + (void)&_modes; + + // Initialize serial port + if (m_hardwareSerial) { + m_hardwareSerial->begin(115200); + } else if (m_softwareSerial) { + m_softwareSerial->begin(115200); + } + + // Initialize strip + m_strip->init(); + if (m_power) { + m_strip->setBrightness(m_brightness); + } else { + m_strip->setBrightness(0); + } + m_strip->setSpeed(m_speed); + m_strip->setColor(0xff, 0xff, 0xff); + m_strip->setMode(FX_MODE_STATIC); + m_strip->start(); + + m_previouseTime = millis(); + sendReadyNotification(); +} + +void NymeaLight::process() +{ + // Read incoming data if there is any + readData(); + + //doAnimations(); + + // Perform service for WS2812FX + m_strip->service(); +} + +void NymeaLight::doAnimations() +{ + // Perform animation steps every 50 ms + uint32_t currentTimestamp = millis(); + if (m_previouseTime - currentTimestamp > 50000) { + //debugPrint("Tick"); + m_previouseTime = currentTimestamp; + + if (m_brightnessFadeDuration > 0 && m_brightnessTargetValue != m_strip->getBrightness()) { + // Calculate step for brightness animation + if (m_brightnessStartValue < m_brightnessTargetValue) { + m_strip->setBrightness(m_strip->getBrightness() - 1); + } else { + m_strip->setBrightness(m_strip->getBrightness() + 1); + } + } + } +} + +void NymeaLight::debugPrint(const char message[]) +{ + streamByte(SlipProtocolEnd, true); + streamByte(NotificationDebugMessage); + streamByte(m_notificationId); + m_notificationId++; + for (size_t i = 0; i < strlen(message); i++) { + streamByte(message[i]); + } + streamByte(SlipProtocolEnd, true); + flushSerial(); +} + +void NymeaLight::flushSerial() +{ + if (m_hardwareSerial) { + m_hardwareSerial->flush(); + } else if (m_softwareSerial) { + m_softwareSerial->flush(); + } +} + +void NymeaLight::sendReadyNotification() +{ + streamByte(SlipProtocolEnd, true); + streamByte(NotificationReady); + streamByte(m_notificationId); + m_notificationId++; + streamByte(SlipProtocolEnd, true); + flushSerial(); +} + +void NymeaLight::readData() +{ + if (m_hardwareSerial) { + while (m_hardwareSerial->available()) { + uint8_t receivedByte = m_hardwareSerial->read(); + processReceivedByte(receivedByte); + } + } else if (m_softwareSerial) { + while (m_softwareSerial->available()) { + uint8_t receivedByte = m_softwareSerial->read(); + processReceivedByte(receivedByte); + } + } +} + +void NymeaLight::processData(uint8_t buffer[], uint8_t length) +{ + uint8_t command = buffer[0]; + uint8_t requestId = buffer[1]; + + switch (command) { + case CommandGetStatus: { + if (length != 2) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandSetPower: { + if (length != 5) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + // uint16_t fadeDuration = (buffer[2] << 8 ) | buffer[3]; + uint8_t powerInt = buffer[4]; + if (powerInt == 0) { + m_power = false; + } else { + m_power = true; + } + + if (m_power) { + m_strip->setBrightness(m_brightness); + } else { + m_strip->setBrightness(0); + } + + // TODO: animate brightness with fade duration + + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandSetColor: { + if (length != 7) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + //uint32_t currentColor = m_strip->getColor(); + //uint32_t targetColor = ((uint32_t) << 16) | ((uint32_t)buffer[5] << 8) | buffer[6]; + // uint16_t fadeDuration = (buffer[2] << 8 ) | buffer[3]; + uint8_t red = buffer[4]; + uint8_t green = buffer[5]; + uint8_t blue = buffer[6]; + m_strip->setColor(red, green, blue); + delayMicroseconds(1000); + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandSetBrightness: { + if (length != 5) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + //uint16_t fadeDuration = (buffer[2] << 8 ) | buffer[3]; + // m_brightnessStartValue = m_strip->getBrightness(); + + m_brightness = buffer[4]; + if (m_power) { + m_strip->setBrightness(m_brightness); + } + + delayMicroseconds(1000); + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandSetSpeed: { + if (length != 6) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + // uint16_t fadeDuration = (buffer[2] << 8 ) | buffer[3]; + uint16_t speed = (buffer[4] << 8 ) | buffer[5]; + m_strip->setSpeed(speed); + delayMicroseconds(1000); + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandSetEffect: { + if (length != 3) { + sendResponse(command, requestId, StatusInvalidPlayload); + return; + } + + m_strip->setMode(buffer[2]); + delayMicroseconds(1000); + sendResponse(command, requestId, StatusSuccess); + break; + } + + case CommandCustom: + + break; + + default: + sendResponse(command, requestId, StatusInvalidCommand); + break; + } + + m_strip->show(); + m_strip->service(); +} + +void NymeaLight::sendResponse(uint8_t command, uint8_t requestId, Status status) +{ + streamByte(SlipProtocolEnd, true); + streamByte(command); + streamByte(requestId); + streamByte(status); + streamByte(SlipProtocolEnd, true); + + flushSerial(); +} + +void NymeaLight::processReceivedByte(uint8_t receivedByte) +{ + if (m_protocolEscaping) { + switch (receivedByte) { + case SlipProtocolTransposedEnd: + m_buffer[m_bufferIndex++] = SlipProtocolEnd; + m_protocolEscaping = false; + break; + case SlipProtocolTransposedEsc: + m_buffer[m_bufferIndex++] = 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 + if (m_bufferIndex > 0) { + processData(m_buffer, m_bufferIndex); + } + m_bufferIndex = 0; + 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[m_bufferIndex++] = receivedByte; + break; + } +} + +void NymeaLight::streamByte(uint8_t dataByte, boolean specialCharacter) +{ + // If this is a special character, write it without escaping + if (specialCharacter) { + writeByte(dataByte); + } else { + switch (dataByte) { + case SlipProtocolEnd: + writeByte(SlipProtocolEsc); + writeByte(SlipProtocolTransposedEnd); + break; + case SlipProtocolEsc: + writeByte(SlipProtocolEsc); + writeByte(SlipProtocolTransposedEsc); + break; + default: + writeByte(dataByte); + break; + } + } +} + +void NymeaLight::writeByte(uint8_t dataByte) +{ + if (m_hardwareSerial) { + m_hardwareSerial->write(dataByte); + } else if (m_softwareSerial) { + m_softwareSerial->write(dataByte); + } +} diff --git a/ws2812fx/firmware/src/nymealight.h b/ws2812fx/firmware/src/nymealight.h new file mode 100644 index 00000000..9c50f486 --- /dev/null +++ b/ws2812fx/firmware/src/nymealight.h @@ -0,0 +1,166 @@ + +/* + SLIP transfere: https://tools.ietf.org/html/rfc1055 + + Request package format: + -------------------------- + uint8 : command (< 0xF0) + uint8 : requestId + uint8[] : payload (dynamic size, max: 253) + + + Response format: + -------------------------- + uint8 : command (< 0xF0) + uint8 : requestId (same as request) + uint8 : status + uint8[] : payload (optional, max: 253) + + + Notification package format: + -------------------------- + uint8 : command (>= 0xF0) + uint8 : notificationId + uint8[] : payload (dynamic size, max: 253) + + + Commands: + -------------------------- + 0x00: get ready status + payload: empty + + + 0x01: set power + payload: + uint16 : fade duration in ms + uint8 : power (0x00 = off, 0x01 = on) + + + 0x02: set color + payload: + uint16 : fade duration in ms + uint8 : red + uint8 : green + uint8 : blue + + + 0x03: set brightness + payload: + uint16 : fade duration in ms + uint8 : brightness (0-255) + + + 0x04: set speed + payload: + uint16 : fade duration in ms + uint16 : speed in ms (big endian) + + + 0x05: set effect + payload: + uint8 : effect, see effect list from WS2812FX + + + 0xef: custom command + The payload can be defined free and custom functionality can be implemented + + + Notifications: + -------------------------- + 0xF0: Status ready notification + payload: empty + + + 0xF1: Debug message notification + payload: debug message characters + + +*/ + +#include +#include +#include + +class NymeaLight +{ +public: + enum Command { + CommandGetStatus = 0x00, + CommandSetPower = 0x01, + CommandSetColor = 0x02, + CommandSetBrightness = 0x03, + CommandSetSpeed = 0x04, + CommandSetEffect = 0x05, + CommandCustom = 0xEF + }; + + enum Notification { + NotificationReady = 0xF0, + NotificationDebugMessage = 0xF1 + }; + + enum Status { + StatusSuccess = 0x00, + StatusInvalidProtocol = 0x01, + StatusInvalidCommand = 0x02, + StatusInvalidPlayload = 0x03, + StatusUnknownError = 0xff + }; + + NymeaLight(WS2812FX *strip); + NymeaLight(SoftwareSerial *serial, WS2812FX *strip); + NymeaLight(HardwareSerial &serial, WS2812FX *strip); + + WS2812FX *strip() const; + + void init(); + void process(); + +private: + enum SlipProtocol { + SlipProtocolEnd = 0xC0, + SlipProtocolEsc = 0xDB, + SlipProtocolTransposedEnd = 0xDC, + SlipProtocolTransposedEsc = 0xDD + }; + + HardwareSerial *m_hardwareSerial = nullptr; + SoftwareSerial *m_softwareSerial = nullptr; + WS2812FX *m_strip = nullptr; + uint8_t m_notificationId = 0; + + // Light states + bool m_power = false; + uint8_t m_brightness = 0xff; + uint32_t m_color = 0x00ffffff; + uint8_t m_effect = FX_MODE_STATIC; + uint16_t m_speed = 2000; + + // UART read + uint8_t m_buffer[255]; + uint8_t m_bufferIndex = 0; + boolean m_protocolEscaping = false; + + // Animations + uint32_t m_previouseTime = 0; + + // Brightness animation + uint32_t m_brightnessProgress = 0; // us + uint8_t m_brightnessStartValue = 0; + uint8_t m_brightnessTargetValue = 0; + uint8_t m_brightnessFadeDuration = 0; //ms + + void doAnimations(); + void debugPrint(const char message[]); + void flushSerial(); + void sendReadyNotification(); + +protected: + virtual void readData(); + virtual void processReceivedByte(uint8_t receivedByte); + virtual void processData(uint8_t buffer[], uint8_t length); + virtual void sendResponse(uint8_t command, uint8_t requestId, Status status); + virtual void streamByte(uint8_t dataByte, boolean specialCharacter = false); + virtual void writeByte(uint8_t dataByte); + +}; diff --git a/ws2812fx/firmware/test/README b/ws2812fx/firmware/test/README new file mode 100644 index 00000000..b94d0890 --- /dev/null +++ b/ws2812fx/firmware/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/ws2812fx/integrationpluginws2812fx.cpp b/ws2812fx/integrationpluginws2812fx.cpp index 61620e0a..71c02f8d 100644 --- a/ws2812fx/integrationpluginws2812fx.cpp +++ b/ws2812fx/integrationpluginws2812fx.cpp @@ -28,22 +28,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -/*! - \page ws2812fx.html - \title WS2812FX Control - \brief Plug-In to control WS2812FX over USB - \ingroup plugins - \ingroup nymea-plugins - - \chapter Plugin properties - Following JSON file contains the definition and the description of all available \l{ThingClass}{DeviceClasses} - and \l{Vendor}{Vendors} of this \l{DevicePlugin}. - - For more details how to read this JSON file please check out the documentation for \l{The plugin JSON File}. -s - \quotefile plugins/deviceplugins/ws2812fx/devicepluginws2812fx.json -*/ #include #include @@ -60,34 +45,46 @@ void IntegrationPluginWs2812fx::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - QString interface = thing->paramValue(ws2812fxThingSerialPortParamTypeId).toString(); + if (thing->thingClassId() == nymeaLightSerialThingClassId) { + QString interface = thing->paramValue(nymeaLightSerialThingSerialPortParamTypeId).toString(); - if (m_usedInterfaces.contains(interface)) { - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("This serial port is already used.")); - return; + if (m_usedInterfaces.contains(interface)) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("This serial port is already in use.")); + return; + } + + NymeaLightSerialInterface *lightInterface = new NymeaLightSerialInterface(interface, thing); + NymeaLight *light = new NymeaLight(lightInterface, this); + lightInterface->setParent(light); + m_usedInterfaces.append(interface); + m_lights.insert(thing, light); + + + connect(light, &NymeaLight::availableChanged, thing, [=](bool available){ + qCDebug(dcWs2812fx()) << thing << "available changed" << available; + thing->setStateValue(nymeaLightSerialConnectedStateTypeId, available); + + if (available) { + // Set the light to the current states + light->setPower(thing->stateValue(nymeaLightSerialPowerStateTypeId).toBool()); + light->setBrightness(thing->stateValue(nymeaLightSerialBrightnessStateTypeId).toUInt()); + light->setColor(thing->stateValue(nymeaLightSerialColorStateTypeId).value()); + light->setEffect(thing->stateValue(nymeaLightSerialEffectModeStateTypeId).toUInt()); + light->setSpeed(thing->stateValue(nymeaLightSerialSpeedStateTypeId).toUInt()); + } + }); + + + info->finish(Thing::ThingErrorNoError); } +} - NymeaLightSerialInterface *lightInterface = new NymeaLightSerialInterface(interface, thing); - NymeaLight *light = new NymeaLight(lightInterface, this); - lightInterface->setParent(light); - - if (!lightInterface->open()) { - qCWarning(dcWs2812fx()) << "Could not open interface" << interface; - light->deleteLater(); - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error opening serial port.")); - return; +void IntegrationPluginWs2812fx::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == nymeaLightSerialThingClassId) { + NymeaLight *light = m_lights.value(thing); + light->enable(); } - - connect(light, &NymeaLight::availableChanged, thing, [=](bool available){ - qCDebug(dcWs2812fx()) << thing << "available changed" << available; - thing->setStateValue(ws2812fxConnectedStateTypeId, available); - }); - - qCDebug(dcWs2812fx()) << "Setup successfully serial port" << interface; - thing->setStateValue(ws2812fxConnectedStateTypeId, true); - m_usedInterfaces.append(interface); - m_lights.insert(thing, light); - info->finish(Thing::ThingErrorNoError); } @@ -96,15 +93,14 @@ void IntegrationPluginWs2812fx::discoverThings(ThingDiscoveryInfo *info) // Create the list of available serial interfaces Q_FOREACH(QSerialPortInfo port, QSerialPortInfo::availablePorts()) { - qCDebug(dcWs2812fx()) << "Found serial port:" << port.portName(); - QString description = port.manufacturer() + " " + port.description(); - ThingDescriptor descriptor(info->thingClassId(), port.portName(), description); - foreach (Thing *existingThing, myThings().filterByParam(ws2812fxThingSerialPortParamTypeId, port.portName())) { + QString description = port.systemLocation() + " " + port.manufacturer() + " " + port.description(); + ThingDescriptor descriptor(info->thingClassId(), QT_TR_NOOP("Nymea light"), description); + foreach (Thing *existingThing, myThings().filterByParam(nymeaLightSerialThingSerialPortParamTypeId, port.systemLocation())) { descriptor.setThingId(existingThing->id()); } ParamList parameters; - parameters.append(Param(ws2812fxThingSerialPortParamTypeId, port.portName())); + parameters.append(Param(nymeaLightSerialThingSerialPortParamTypeId, port.systemLocation())); descriptor.setParams(parameters); info->addThingDescriptor(descriptor); } @@ -122,16 +118,11 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) return; } - if (action.actionTypeId() == ws2812fxPowerActionTypeId) { - bool power = action.param(ws2812fxPowerActionPowerParamTypeId).value().toBool(); - quint8 brightness = 0; - if (power) { - quint8 brightnessPercentage = thing->stateValue(ws2812fxBrightnessStateTypeId).toUInt(); - brightness = qRound(255.0 * brightnessPercentage / 100); - } + if (action.actionTypeId() == nymeaLightSerialPowerActionTypeId) { + bool power = action.param(nymeaLightSerialPowerActionPowerParamTypeId).value().toBool(); qCDebug(dcWs2812fx()) << "Set power" << power; - NymeaLightInterfaceReply *reply = light->setBrightness(brightness, 500); + NymeaLightInterfaceReply *reply = light->setPower(power, 500); connect(info, &ThingActionInfo::aborted, reply, &NymeaLightInterfaceReply::finished); connect(reply, &NymeaLightInterfaceReply::finished, this, [=](){ if (reply->status() != NymeaLightInterface::StatusSuccess) { @@ -140,11 +131,11 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) } qCDebug(dcWs2812fx()) << "Set power finished successfully" << power; - thing->setStateValue(ws2812fxPowerStateTypeId, power); + thing->setStateValue(nymeaLightSerialPowerStateTypeId, power); info->finish(Thing::ThingErrorNoError); }); - } else if (action.actionTypeId() == ws2812fxColorActionTypeId) { - QColor color = action.param(ws2812fxColorActionColorParamTypeId).value().value(); + } else if (action.actionTypeId() == nymeaLightSerialColorActionTypeId) { + QColor color = action.param(nymeaLightSerialColorActionColorParamTypeId).value().value(); qCDebug(dcWs2812fx()) << "Set color to" << color.name(QColor::HexRgb); NymeaLightInterfaceReply *reply = light->setColor(color); connect(info, &ThingActionInfo::aborted, reply, &NymeaLightInterfaceReply::finished); @@ -154,12 +145,12 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) return; } qCDebug(dcWs2812fx()) << "Set color finished successfully" << color.name(QColor::HexRgb); - thing->setStateValue(ws2812fxColorStateTypeId, color); + thing->setStateValue(nymeaLightSerialColorStateTypeId, color); info->finish(Thing::ThingErrorNoError); }); - } else if (action.actionTypeId() == ws2812fxColorTemperatureActionTypeId) { + } else if (action.actionTypeId() == nymeaLightSerialColorTemperatureActionTypeId) { // minValue 153, maxValue 500 - uint colorTemperature = action.param(ws2812fxColorTemperatureActionColorTemperatureParamTypeId).value().toDouble(); + uint colorTemperature = action.param(nymeaLightSerialColorTemperatureActionColorTemperatureParamTypeId).value().toDouble(); QColor color; color.setRgb(255, 255, qRound((255.00 - (((colorTemperature - 153.00) / 347.00)) * 255.00))); @@ -173,11 +164,11 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) } qCDebug(dcWs2812fx()) << "Set color temperature finished successfully" << colorTemperature; - thing->setStateValue(ws2812fxColorTemperatureStateTypeId, colorTemperature); + thing->setStateValue(nymeaLightSerialColorTemperatureStateTypeId, colorTemperature); info->finish(Thing::ThingErrorNoError); }); - } else if (action.actionTypeId() == ws2812fxBrightnessActionTypeId) { - quint8 brightnessPercentage = action.param(ws2812fxBrightnessActionBrightnessParamTypeId).value().toUInt(); + } else if (action.actionTypeId() == nymeaLightSerialBrightnessActionTypeId) { + quint8 brightnessPercentage = action.param(nymeaLightSerialBrightnessActionBrightnessParamTypeId).value().toUInt(); quint8 brightness = qRound(255.0 * brightnessPercentage / 100); qCDebug(dcWs2812fx()) << "Set brightness to" << brightnessPercentage << brightness; NymeaLightInterfaceReply *reply = light->setBrightness(brightness, 1000); @@ -189,11 +180,11 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) } qCDebug(dcWs2812fx()) << "Set brightness finished successfully" << brightness; - thing->setStateValue(ws2812fxBrightnessStateTypeId, brightnessPercentage); + thing->setStateValue(nymeaLightSerialBrightnessStateTypeId, brightnessPercentage); info->finish(Thing::ThingErrorNoError); }); - } else if (action.actionTypeId() == ws2812fxSpeedActionTypeId) { - quint16 speedPercentage = action.param(ws2812fxSpeedActionSpeedParamTypeId).value().toUInt(); + } else if (action.actionTypeId() == nymeaLightSerialSpeedActionTypeId) { + quint16 speedPercentage = action.param(nymeaLightSerialSpeedActionSpeedParamTypeId).value().toUInt(); quint16 speed = 2000 - (speedPercentage * 20); qCDebug(dcWs2812fx()) << "Set speed" << speedPercentage << "%" << speed << "ms"; @@ -206,11 +197,11 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) } qCDebug(dcWs2812fx()) << "Set speed finished successfully" << speedPercentage << "%" << speed << "ms"; - thing->setStateValue(ws2812fxSpeedStateTypeId, speedPercentage); + thing->setStateValue(nymeaLightSerialSpeedStateTypeId, speedPercentage); info->finish(Thing::ThingErrorNoError); }); - } else if (action.actionTypeId() == ws2812fxEffectModeActionTypeId) { - QString effectMode = action.param(ws2812fxEffectModeActionEffectModeParamTypeId).value().toString(); + } else if (action.actionTypeId() == nymeaLightSerialEffectModeActionTypeId) { + QString effectMode = action.param(nymeaLightSerialEffectModeActionEffectModeParamTypeId).value().toString(); quint8 mode = FX_MODE_STATIC; if (effectMode == "Static") { mode = FX_MODE_STATIC; @@ -340,7 +331,7 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) } qCDebug(dcWs2812fx()) << "Set mode finished successfully" << effectMode << mode; - thing->setStateValue(ws2812fxEffectModeStateTypeId, effectMode); + thing->setStateValue(nymeaLightSerialEffectModeStateTypeId, effectMode); info->finish(Thing::ThingErrorNoError); }); } @@ -349,9 +340,8 @@ void IntegrationPluginWs2812fx::executeAction(ThingActionInfo *info) void IntegrationPluginWs2812fx::thingRemoved(Thing *thing) { - if (thing->thingClassId() == ws2812fxThingClassId) { - - m_usedInterfaces.removeAll(thing->paramValue(ws2812fxThingSerialPortParamTypeId).toString()); + if (thing->thingClassId() == nymeaLightSerialThingClassId) { + m_usedInterfaces.removeAll(thing->paramValue(nymeaLightSerialThingSerialPortParamTypeId).toString()); NymeaLight *light = m_lights.take(thing); light->deleteLater(); } diff --git a/ws2812fx/integrationpluginws2812fx.h b/ws2812fx/integrationpluginws2812fx.h index 139b0631..29ccd6ac 100644 --- a/ws2812fx/integrationpluginws2812fx.h +++ b/ws2812fx/integrationpluginws2812fx.h @@ -111,6 +111,7 @@ public: explicit IntegrationPluginWs2812fx(); void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void discoverThings(ThingDiscoveryInfo *info) override; void executeAction(ThingActionInfo *info) override; diff --git a/ws2812fx/integrationpluginws2812fx.json b/ws2812fx/integrationpluginws2812fx.json index c7de2fd6..794d31b7 100644 --- a/ws2812fx/integrationpluginws2812fx.json +++ b/ws2812fx/integrationpluginws2812fx.json @@ -10,8 +10,8 @@ "thingClasses": [ { "id": "24364e36-b199-4e35-b468-c44da58c009c", - "name": "ws2812fx", - "displayName": "WS2812FX", + "name": "nymeaLightSerial", + "displayName": "Serial nymea light", "createMethods": ["user", "discovery"], "interfaces": ["colorlight", "connectable"], "paramTypes": [ @@ -21,7 +21,7 @@ "displayName": "Serial port", "type": "QString", "inputType": "TextLine", - "defaultValue": "ttyAMC0" + "defaultValue": "/dev/ttyAMC0" } ], "stateTypes": [ @@ -63,7 +63,7 @@ "displayNameEvent": "color changed", "displayNameAction": "Set color", "type": "QColor", - "defaultValue": "#000000", + "defaultValue": "#FFFFFF", "writable": true }, { @@ -74,7 +74,7 @@ "displayNameAction": "Set brigtness", "type": "int", "unit": "Percentage", - "defaultValue": 0, + "defaultValue": 100, "minValue": 0, "maxValue": 100, "writable": true @@ -87,7 +87,7 @@ "displayNameAction": "Set speed", "type": "int", "unit": "Percentage", - "defaultValue": 0, + "defaultValue": 50, "minValue": 0, "maxValue": 100, "writable": true diff --git a/ws2812fx/nymealight.cpp b/ws2812fx/nymealight.cpp index a92d11b5..47436b9e 100644 --- a/ws2812fx/nymealight.cpp +++ b/ws2812fx/nymealight.cpp @@ -7,12 +7,36 @@ NymeaLight::NymeaLight(NymeaLightInterface *interface, QObject *parent) : QObject(parent), m_interface(interface) { + connect(m_interface, &NymeaLightInterface::dataReceived, this, &NymeaLight::onDataReceived); connect(m_interface, &NymeaLightInterface::availableChanged, this, [=](bool available){ - qCDebug(dcWs2812fx()) << "Interface available changed" << available; - emit availableChanged(available); + m_interfaceAvailable = available; + if (m_interfaceAvailable) { + qCDebug(dcWs2812fx()) << "Nymea light interface is now available. Start polling status of the light controller..."; + m_pollStatusRetryCount = 0; + pollStatus(); + } else { + m_ready = false; + m_requestId = 0; + emit availableChanged(false); + } }); - connect(m_interface, &NymeaLightInterface::dataReceived, this, &NymeaLight::onDataReceived); +} + +NymeaLightInterfaceReply *NymeaLight::setPower(bool power, quint16 fadeDuration) +{ + // Build the request + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(NymeaLightInterface::CommandSetPower); + stream << m_requestId++; + stream << fadeDuration; + stream << static_cast(power ? 0x01 : 0x00); + + NymeaLightInterfaceReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; } NymeaLightInterfaceReply *NymeaLight::setColor(const QColor &color, quint16 fadeDuration) @@ -82,7 +106,19 @@ NymeaLightInterfaceReply *NymeaLight::setEffect(quint8 effect) bool NymeaLight::available() const { - return m_interface->available(); + return m_interfaceAvailable && m_ready; +} + +void NymeaLight::enable() +{ + m_interface->open(); + qCDebug(dcWs2812fx()) << "Nymea light enabled"; +} + +void NymeaLight::disable() +{ + m_interface->close(); + qCDebug(dcWs2812fx()) << "Nymea light disabled"; } NymeaLightInterfaceReply *NymeaLight::createReply(const QByteArray &requestData) @@ -114,36 +150,93 @@ void NymeaLight::sendNextRequest() m_currentReply->m_timer->start(); } +void NymeaLight::pollStatus() +{ + // Request ready state from controller + NymeaLightInterfaceReply *reply = getStatus(); + connect(reply, &NymeaLightInterfaceReply::finished, this, [=](){ + if (reply->status() == NymeaLightInterface::StatusSuccess) { + qCDebug(dcWs2812fx()) << "Get status request finished successfully. The firmware is ready to operate."; + m_ready = true; + m_pollStatusRetryCount = 0; + emit availableChanged(true); + } else { + m_pollStatusRetryCount++; + if (m_pollStatusRetryCount >= m_pollStatusRetryLimit) { + qCWarning(dcWs2812fx()) << "Firmware did not respond to get status request after" << m_pollStatusRetryCount << "attempts. Giving up."; + m_ready = false; + } else { + if (!m_ready && m_interfaceAvailable) { + qCDebug(dcWs2812fx()) << "Get status request finished with error" << reply->status() << "Retry" << m_pollStatusRetryCount << "/" << m_pollStatusRetryLimit; + pollStatus(); + } else { + qCDebug(dcWs2812fx()) << "Get status request finished with error, but that's ok since we received the ready notification." << reply->status(); + } + } + } + }); +} + +NymeaLightInterfaceReply *NymeaLight::getStatus() +{ + qCDebug(dcWs2812fx()) << "Request status of nymea light"; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(NymeaLightInterface::CommandGetStatus); + stream << m_requestId++; + + NymeaLightInterfaceReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + void NymeaLight::onDataReceived(const QByteArray &data) { Q_ASSERT(data.length() >= 3); - NymeaLightInterface::Command command = static_cast(static_cast((data.at(0)))); + quint8 commandInt = static_cast((data.at(0))); quint8 requestId = static_cast(data.at(1)); - //qCDebug(dcWs2812fx()) << "Recived data" << command << requestId << data.toHex(); - if (command == NymeaLightInterface::CommandDebug) { - qCDebug(dcWs2812fx()) << "Firmware debug:" << QString::fromUtf8(data.right(data.length() - 2)); - return; - } + qCDebug(dcWs2812fx()) << "Recived data" << commandInt << requestId << data.toHex(); - NymeaLightInterface::Status status = static_cast(data.at(2)); + // Check if command or notification + if (commandInt < 0xF0) { + NymeaLightInterface::Command command = static_cast(commandInt); - if (m_currentReply) { - if (m_currentReply->command() == command && m_currentReply->requestId() == requestId) { - m_currentReply->m_timer->stop(); - m_currentReply->m_status = status; + NymeaLightInterface::Status status = static_cast(data.at(2)); - if (status != NymeaLightInterface::StatusSuccess) { - qCWarning(dcWs2812fx()) << "Request finished with error" << command << m_currentReply->requestId() << status; - } else { - qCDebug(dcWs2812fx()) << "Request finished" << command << m_currentReply->requestId() << status; + if (m_currentReply) { + if (m_currentReply->command() == command && m_currentReply->requestId() == requestId) { + m_currentReply->m_timer->stop(); + m_currentReply->m_status = status; + + if (status != NymeaLightInterface::StatusSuccess) { + qCWarning(dcWs2812fx()) << "Request finished with error" << command << m_currentReply->requestId() << status; + } else { + qCDebug(dcWs2812fx()) << "Request finished" << command << m_currentReply->requestId() << status; + } + + emit m_currentReply->finished(); } - - emit m_currentReply->finished(); + } else { + qCWarning(dcWs2812fx()) << "Received unhandled command response data" << data.toHex(); } } else { - qCWarning(dcWs2812fx()) << "Received unhandled data" << data.toHex(); + NymeaLightInterface::Notification notification = static_cast(commandInt); + switch (notification) { + case NymeaLightInterface::NotificationReady: + qCDebug(dcWs2812fx()) << "Controller ready notification received"; + m_ready = true; + emit availableChanged(true); + break; + case NymeaLightInterface::NotificationDebugMessage: + qCDebug(dcWs2812fx()) << "Firmware debug:" << QString::fromUtf8(data.right(data.length() - 2)); + break; + default: + qCWarning(dcWs2812fx()) << "Unhandled notification received" << data.toHex(); + break; + } } } diff --git a/ws2812fx/nymealight.h b/ws2812fx/nymealight.h index 9f5584c7..f1cc848d 100644 --- a/ws2812fx/nymealight.h +++ b/ws2812fx/nymealight.h @@ -15,6 +15,7 @@ public: // 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 *setPower(bool power, quint16 fadeDuration = 0); NymeaLightInterfaceReply *setColor(const QColor &color, quint16 fadeDuration = 0); NymeaLightInterfaceReply *setBrightness(quint8 brightness, quint16 fadeDuration = 0); NymeaLightInterfaceReply *setSpeed(quint16 speed, quint16 fadeDuration = 0); @@ -22,9 +23,17 @@ public: bool available() const; +public slots: + void enable(); + void disable(); + private: NymeaLightInterface *m_interface = nullptr; quint8 m_requestId = 0; + bool m_interfaceAvailable = false; + bool m_ready = false; + int m_pollStatusRetryCount = 0; + int m_pollStatusRetryLimit = 5; NymeaLightInterfaceReply *m_currentReply = nullptr; QQueue m_pendingRequests; @@ -32,6 +41,11 @@ private: NymeaLightInterfaceReply *createReply(const QByteArray &requestData); void sendNextRequest(); + + void pollStatus(); + NymeaLightInterfaceReply *getStatus(); + + private slots: void onDataReceived(const QByteArray &data); diff --git a/ws2812fx/nymealightinterface.cpp b/ws2812fx/nymealightinterface.cpp deleted file mode 100644 index e80cfc33..00000000 --- a/ws2812fx/nymealightinterface.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "nymealightinterface.h" - -NymeaLightInterface::NymeaLightInterface(QObject *parent) : QObject(parent) -{ - -} diff --git a/ws2812fx/nymealightinterface.h b/ws2812fx/nymealightinterface.h index 48dc0399..d088d1a2 100644 --- a/ws2812fx/nymealightinterface.h +++ b/ws2812fx/nymealightinterface.h @@ -9,15 +9,22 @@ class NymeaLightInterface : public QObject Q_OBJECT public: enum Command { - CommandSetColor = 0x00, - CommandSetBrightness = 0x01, - CommandSetSpeed = 0x02, - CommandSetEffect = 0x03, - CommandDebug = 0xFE, - CommandCustom = 0xFF + CommandGetStatus = 0x00, + CommandSetPower = 0x01, + CommandSetColor = 0x02, + CommandSetBrightness = 0x03, + CommandSetSpeed = 0x04, + CommandSetEffect = 0x05, + CommandCustom = 0xEF }; Q_ENUM(Command) + enum Notification { + NotificationReady = 0xF0, + NotificationDebugMessage = 0xF1 + }; + Q_ENUM(Notification) + enum Status { StatusSuccess = 0x00, StatusInvalidProtocol = 0x01, @@ -34,7 +41,8 @@ public: }; Q_ENUM(Mode) - explicit NymeaLightInterface(QObject *parent = nullptr); + inline explicit NymeaLightInterface(QObject *parent = nullptr) : QObject(parent) { }; + virtual ~NymeaLightInterface() = default; virtual bool open() = 0; virtual void close() = 0; diff --git a/ws2812fx/nymealightserialinterface.cpp b/ws2812fx/nymealightserialinterface.cpp index 0ab8e928..ea831516 100644 --- a/ws2812fx/nymealightserialinterface.cpp +++ b/ws2812fx/nymealightserialinterface.cpp @@ -4,9 +4,10 @@ #include NymeaLightSerialInterface::NymeaLightSerialInterface(const QString &name, QObject *parent) : - NymeaLightInterface(parent) + NymeaLightInterface(parent), + m_serialPortName(name) { - m_serialPort = new QSerialPort(name, this); + m_serialPort = new QSerialPort(m_serialPortName, this); m_serialPort->setBaudRate(115200); m_serialPort->setDataBits(QSerialPort::DataBits::Data8); m_serialPort->setParity(QSerialPort::Parity::NoParity); @@ -33,8 +34,26 @@ NymeaLightSerialInterface::NymeaLightSerialInterface(const QString &name, QObjec bool NymeaLightSerialInterface::open() { + bool serialPortFound = false; + foreach(const QSerialPortInfo &serialPortInfo, QSerialPortInfo::availablePorts()) { + if (serialPortInfo.systemLocation() == m_serialPortName) { + serialPortFound = true; + break; + } + } + + // Prevent repeating warnings... + if (!serialPortFound) { + if (!m_reconnectTimer->isActive()) { + m_reconnectTimer->start(); + } + return false; + } + + // The serial port is available...lt's try to open it if (!m_serialPort->open(QIODevice::ReadWrite)) { qCWarning(dcWs2812fx()) << "Could not open serial port" << m_serialPort->portName() << m_serialPort->errorString(); + m_reconnectTimer->start(); return false; } @@ -130,7 +149,7 @@ void NymeaLightSerialInterface::onReadyRead() void NymeaLightSerialInterface::onSerialError(QSerialPort::SerialPortError error) { if (error != QSerialPort::NoError && m_serialPort->isOpen()) { - qCCritical(dcWs2812fx()) << "Serial port error:" << error << m_serialPort->errorString(); + qCWarning(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 index 2def124a..beb4863a 100644 --- a/ws2812fx/nymealightserialinterface.h +++ b/ws2812fx/nymealightserialinterface.h @@ -1,9 +1,10 @@ #ifndef NYMEALIGHTSERIALINTERFACE_H #define NYMEALIGHTSERIALINTERFACE_H +#include #include #include -#include +#include #include "nymealightinterface.h" @@ -12,10 +13,12 @@ class NymeaLightSerialInterface : public NymeaLightInterface Q_OBJECT public: explicit NymeaLightSerialInterface(const QString &name, QObject *parent = nullptr); + ~NymeaLightSerialInterface() override = default; bool open() override; void close() override; bool available() override; + void sendData(const QByteArray &data) override; private: @@ -26,6 +29,7 @@ private: SlipProtocolTransposedEsc = 0xDD }; + QString m_serialPortName; QTimer *m_reconnectTimer = nullptr; QSerialPort *m_serialPort = nullptr; QByteArray m_buffer; diff --git a/ws2812fx/ws2812fx.pro b/ws2812fx/ws2812fx.pro index 62c8a873..33f5bbd8 100644 --- a/ws2812fx/ws2812fx.pro +++ b/ws2812fx/ws2812fx.pro @@ -7,7 +7,6 @@ TARGET = $$qtLibraryTarget(nymea_integrationpluginws2812fx) SOURCES += \ integrationpluginws2812fx.cpp \ nymealight.cpp \ - nymealightinterface.cpp \ nymealightserialinterface.cpp