From f65bf428d9286bb4782092c2c0d84e91b972201a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 9 Mar 2022 08:21:05 +0100 Subject: [PATCH] Add basic structure for USB-RLY82 --- nymea-plugins.pro | 1 + usbrly82/README.md | 23 ++ usbrly82/integrationpluginusbrly82.cpp | 254 ++++++++++++++++++ usbrly82/integrationpluginusbrly82.h | 67 +++++ usbrly82/integrationpluginusbrly82.json | 245 +++++++++++++++++ usbrly82/meta.json | 13 + usbrly82/robotelectronics.png | Bin 0 -> 11898 bytes usbrly82/serialportmonitor.cpp | 190 ++++++++++++++ usbrly82/serialportmonitor.h | 45 ++++ usbrly82/usbrly82.cpp | 333 ++++++++++++++++++++++++ usbrly82/usbrly82.h | 106 ++++++++ usbrly82/usbrly82.pro | 16 ++ 12 files changed, 1293 insertions(+) create mode 100644 usbrly82/README.md create mode 100644 usbrly82/integrationpluginusbrly82.cpp create mode 100644 usbrly82/integrationpluginusbrly82.h create mode 100644 usbrly82/integrationpluginusbrly82.json create mode 100644 usbrly82/meta.json create mode 100644 usbrly82/robotelectronics.png create mode 100644 usbrly82/serialportmonitor.cpp create mode 100644 usbrly82/serialportmonitor.h create mode 100644 usbrly82/usbrly82.cpp create mode 100644 usbrly82/usbrly82.h create mode 100644 usbrly82/usbrly82.pro diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 781fa5bc..8f528f5a 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -72,6 +72,7 @@ PLUGIN_DIRS = \ udpcommander \ unifi \ usbrelay \ + usbrly82 \ wakeonlan \ wemo \ ws2812fx \ diff --git a/usbrly82/README.md b/usbrly82/README.md new file mode 100644 index 00000000..78ef0b00 --- /dev/null +++ b/usbrly82/README.md @@ -0,0 +1,23 @@ +# USB-RLY82 - 2 channel USB relay + +-------------------------------- + +This plugin allows you to control USB relays and receive digital and analog input values from the controller. + +In order to set up the the controller in nymea, you only need to plug in the device. All digital and analog channels will be set up automatically. +Each relay board has a unique serial number and therefore it can be clearly identified. + +## Supported things + +* USB-RLY82 + +This relay supports: + +* 2 Digital outputs (Relays) +* 8 Digital and analog inputs +* the Resolution of the ADC can be configured + +## More + +Information about the USB relay hardware can be found [here](https://www.robot-electronics.co.uk/usb-rly82.html). +The datasheet can be downloaded from [here](https://www.robot-electronics.co.uk/files/usb-rly82.pdf) diff --git a/usbrly82/integrationpluginusbrly82.cpp b/usbrly82/integrationpluginusbrly82.cpp new file mode 100644 index 00000000..28537829 --- /dev/null +++ b/usbrly82/integrationpluginusbrly82.cpp @@ -0,0 +1,254 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "plugininfo.h" +#include "integrationpluginusbrly82.h" + +IntegrationPluginUsbRly82::IntegrationPluginUsbRly82() +{ + +} + +void IntegrationPluginUsbRly82::init() +{ + m_monitor = new SerialPortMonitor(this); +} + +void IntegrationPluginUsbRly82::discoverThings(ThingDiscoveryInfo *info) +{ + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginUsbRly82::startMonitoringAutoThings() +{ + // Start seaching for devices which can be discovered and added automatically + connect(m_monitor, &SerialPortMonitor::serialPortAdded, this, &IntegrationPluginUsbRly82::onSerialPortAdded); + connect(m_monitor, &SerialPortMonitor::serialPortRemoved, this, &IntegrationPluginUsbRly82::onSerialPortRemoved); + + // Check the initial list of + foreach (const SerialPortMonitor::SerialPortInfo &serialPortInfo, m_monitor->serialPortInfos()) { + onSerialPortAdded(serialPortInfo); + } +} + +void IntegrationPluginUsbRly82::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcUsbRly82()) << "Setup thing" << thing; + + // Relay + if (info->thing()->thingClassId() == usbRelayThingClassId) { + + // Search for the serial port with the given serialnumber + foreach (const SerialPortMonitor::SerialPortInfo &serialPortInfo, m_monitor->serialPortInfos()) { + if (serialPortInfo.serialNumber == thing->paramValue(usbRelayThingSerialNumberParamTypeId).toString()) { + qCDebug(dcUsbRly82()) << "Found serial port for" << thing << serialPortInfo; + + UsbRly82 *relay = new UsbRly82(this); + connect(relay, &UsbRly82::availableChanged, thing, [=](bool available){ + qCDebug(dcUsbRly82()) << thing << "available changed" << available; + thing->setStateValue("connected", available); + }); + + connect(relay, &UsbRly82::powerRelay1Changed, thing, [=](bool power){ + qCDebug(dcUsbRly82()) << thing << "relay 1 power changed" << power; + thing->setStateValue(usbRelayPowerRelay1StateTypeId, power); + }); + + connect(relay, &UsbRly82::powerRelay2Changed, thing, [=](bool power){ + qCDebug(dcUsbRly82()) << thing << "relay 2 power changed" << power; + thing->setStateValue(usbRelayPowerRelay1StateTypeId, power); + }); + + + connect(relay, &UsbRly82::digitalInputsChanged, thing, [=](){ + thing->setStateValue(usbRelayPowerRelay1StateTypeId, UsbRly82::checkBit()); + }); + + + if (!relay->connectRelay(serialPortInfo.systemLocation)) { + qCWarning(dcUsbRly82()) << "Setup failed. Could not connect to relay" << thing; + info->finish(Thing::ThingErrorHardwareFailure); + relay->deleteLater(); + return; + } + + m_relays.insert(thing, relay); + info->finish(Thing::ThingErrorNoError); + return; + } + } + + // Does not seem to be available + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + info->finish(Thing::ThingErrorSetupFailed); +} + + +void IntegrationPluginUsbRly82::postSetupThing(Thing *thing) +{ + qCDebug(dcUsbRly82()) << "Post setup thing" << thing; + + // if (thing->thingClassId() == usbRelayConnectorThingClassId) { + + // // Initialize the states + // UsbRelay *relay = m_relays.key(thing); + // if (!relay) { + // qCWarning(dcUsbRly82()) << "Could not find relay in post setup."; + // return; + // } + + // thing->setStateValue(usbRelayConnectorConnectedStateTypeId, relay->connected()); + + // // Check if we have to create child devices (relays) + // if (myThings().filterByParentId(thing->id()).isEmpty()) { + + // ThingDescriptors descriptors; + // for (int i = 0; i < relay->relayCount(); i++) { + // int relayNumber = i + 1; + // ThingDescriptor descriptor(usbRelayThingClassId, QString("Relay %1").arg(relayNumber), QString(), thing->id()); + // ParamList params; + // params.append(Param(usbRelayThingRelayNumberParamTypeId, relayNumber)); + // descriptor.setParams(params); + // descriptors.append(descriptor); + // } + + // emit autoThingsAppeared(descriptors); + // } + // } else if (thing->thingClassId() == usbRelayThingClassId) { + + // UsbRelay *relay = getRelayForDevice(thing); + // if (!relay) return; + + // // Set the current states + // int relayNumber = thing->paramValue(usbRelayThingRelayNumberParamTypeId).toInt(); + // thing->setStateValue(usbRelayConnectedStateTypeId, relay->connected()); + // thing->setStateValue(usbRelayPowerStateTypeId, relay->relayPower(relayNumber)); + // } +} + +void IntegrationPluginUsbRly82::thingRemoved(Thing *thing) +{ + qCDebug(dcUsbRly82()) << "Remove thing" << thing; + if (thing->thingClassId() == usbRelayThingClassId) { + UsbRly82 *relay = m_relays.take(thing); + if (!relay) return; + delete relay; + } +} + + +void IntegrationPluginUsbRly82::executeAction(ThingActionInfo *info) +{ + qCDebug(dcUsbRly82()) << "Executing action for thing" << info->thing() << info->action().actionTypeId().toString() << info->action().params(); + + if (info->thing()->thingClassId() == usbRelayThingClassId) { + + Thing *thing = info->thing(); + UsbRly82 *relay = m_relays.value(thing); + + if (!relay) { + qCWarning(dcUsbRly82()) << "Could execute action because could not find USB relay for" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (!relay->available()) { + qCWarning(dcUsbRly82()) << "Relay is not connected"; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == usbRelayPowerRelay1ActionTypeId) { + bool power = info->action().paramValue(usbRelayPowerRelay1ActionPowerRelay1ParamTypeId).toBool(); + UsbRly82Reply *reply = relay->setRelay1Power(power); + connect(reply, &UsbRly82Reply::finished, info, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->finish(Thing::ThingErrorNoError); + }); + return; + } else if (info->action().actionTypeId() == usbRelayPowerRelay2ActionTypeId) { + bool power = info->action().paramValue(usbRelayPowerRelay2ActionPowerRelay2ParamTypeId).toBool(); + UsbRly82Reply *reply = relay->setRelay2Power(power); + connect(reply, &UsbRly82Reply::finished, info, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->finish(Thing::ThingErrorNoError); + }); + return; + } + + info->finish(Thing::ThingErrorActionTypeNotFound); + } + + info->finish(Thing::ThingErrorThingClassNotFound); +} + +void IntegrationPluginUsbRly82::onSerialPortAdded(const SerialPortMonitor::SerialPortInfo &serialPortInfo) +{ + if (serialPortInfo.vendorId == 0x04d8 && serialPortInfo.productId == 0xffee) { + qCDebug(dcUsbRly82()) << "[+] Added" << serialPortInfo; + Things alreadyAddedThings = myThings().filterByThingClassId(usbRelayThingClassId).filterByParam(usbRelayThingSerialNumberParamTypeId, serialPortInfo.serialNumber); + if (alreadyAddedThings.isEmpty()) { + qCDebug(dcUsbRly82()) << "New" << serialPortInfo.product << serialPortInfo.serialNumber << "showed up. Setting up a new thing for this."; + ThingDescriptor descriptor(usbRelayThingClassId, "USB-RLY82"); + ParamList params; + params.append(Param(usbRelayThingSerialNumberParamTypeId, serialPortInfo.serialNumber)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } else { + Thing *relayThing = alreadyAddedThings.first(); + if (relayThing) { + qCDebug(dcUsbRly82()) << "Thing already set up for this controller" << relayThing; + UsbRly82 *relay = m_relays.value(relayThing); + if (relay) { + relay->connectRelay(serialPortInfo.systemLocation); + } + } + } + } +} + +void IntegrationPluginUsbRly82::onSerialPortRemoved(const SerialPortMonitor::SerialPortInfo &serialPortInfo) +{ + if (serialPortInfo.vendorId == 0x04d8 && serialPortInfo.productId == 0xffee) { + qCDebug(dcUsbRly82()) << "[-] Removed" << serialPortInfo; + } +} diff --git a/usbrly82/integrationpluginusbrly82.h b/usbrly82/integrationpluginusbrly82.h new file mode 100644 index 00000000..b6cbe587 --- /dev/null +++ b/usbrly82/integrationpluginusbrly82.h @@ -0,0 +1,67 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINUSBRLY82_H +#define INTEGRATIONPLUGINUSBRLY82_H + +#include "integrations/integrationplugin.h" +#include "serialportmonitor.h" +#include "usbrly82.h" + +class IntegrationPluginUsbRly82: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginusbrly82.json") + Q_INTERFACES(IntegrationPlugin) + + +public: + explicit IntegrationPluginUsbRly82(); + + void init() override; + void startMonitoringAutoThings() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + SerialPortMonitor *m_monitor = nullptr; + QHash m_relays; + +private slots: + void onSerialPortAdded(const SerialPortMonitor::SerialPortInfo &serialPortInfo); + void onSerialPortRemoved(const SerialPortMonitor::SerialPortInfo &serialPortInfo); + +}; + +#endif // INTEGRATIONPLUGINUSBRLY82_H diff --git a/usbrly82/integrationpluginusbrly82.json b/usbrly82/integrationpluginusbrly82.json new file mode 100644 index 00000000..5a4f4b95 --- /dev/null +++ b/usbrly82/integrationpluginusbrly82.json @@ -0,0 +1,245 @@ +{ + "name": "UsbRly82", + "displayName": "USB-RLY82", + "id": "33d4ebad-104b-4f24-8089-6edd35ba2c95", + "vendors": [ + { + "displayName": "Robot electronics", + "name": "robotElectronics", + "id": "c3b972d1-85e6-436c-ae07-a04cbfae604e", + "thingClasses": [ + { + "name": "usbRelay", + "displayName": "USB-RLY82", + "id": "0ae5cd75-d42e-4993-8ce8-ced5ba315688", + "setupMethod": "JustAdd", + "createMethods": ["discovery", "auto"], + "interfaces": [ "gateway" ], + "paramTypes": [ + { + "id": "5ac5b15f-34cf-4d42-8b9d-6e9866d8f2ed", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "b482b0ea-1901-4437-a309-be43833a1ad5", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "9935ecba-bb56-4c6c-8be1-60bacc40abb7", + "name": "powerRelay1", + "displayName": "Power relay 1", + "displayNameEvent": "Power relay 1 changed", + "displayNameAction": "Set power relay 1", + "type": "bool", + "ioType": "digitalOutput", + "defaultValue": false, + "writable": true, + "cached": true + }, + { + "id": "d4b84a73-44dd-4bb9-a0d0-e45e6d2aaf7d", + "name": "powerRelay2", + "displayName": "Power relay 2", + "displayNameEvent": "Power relay 2 changed", + "displayNameAction": "Set power relay 2", + "type": "bool", + "ioType": "digitalOutput", + "defaultValue": false, + "writable": true, + "cached": true + }, + { + "id": "ad60f859-11e0-4a80-86fc-1810a5054fb6", + "name": "digitalInputChannel1", + "displayName": "Channel 1", + "displayNameEvent": "Channel 1 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "5038008d-68c7-4cde-845d-335758a05a15", + "name": "digitalInputChannel2", + "displayName": "Channel 2", + "displayNameEvent": "Channel 2 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "73a8c051-5ce8-4909-a75f-f0e88f1ceb61", + "name": "digitalInputChannel3", + "displayName": "Channel 3", + "displayNameEvent": "Channel 3 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "11a3e692-cccc-435b-a9fd-721ae332c90c", + "name": "digitalInputChannel4", + "displayName": "Channel 4", + "displayNameEvent": "Channel 4 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "932cb171-5ba2-4420-bee3-9016dca6498a", + "name": "digitalInputChannel5", + "displayName": "Channel 5", + "displayNameEvent": "Channel 5 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "e10074d7-68cb-4f74-87ea-f0cdf193d207", + "name": "digitalInputChannel6", + "displayName": "Channel 6", + "displayNameEvent": "Channel 6 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "f37aa320-ecfb-4896-bb31-8109606cc5b3", + "name": "digitalInputChannel7", + "displayName": "Channel 7", + "displayNameEvent": "Channel 7 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "32aa1fbd-c128-4258-bb41-22b5b5e150de", + "name": "digitalInputChannel8", + "displayName": "Channel 8", + "displayNameEvent": "Channel 8 input changed", + "type": "bool", + "ioType": "digitalInput", + "defaultValue": false, + "cached": true + }, + { + "id": "0dfda321-7da5-4e4c-880e-5b45098398e8", + "name": "analogInputChannel1", + "displayName": "Analog Channel 1", + "displayNameEvent": "Analog channel 1 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "02882e2a-f0a6-4042-b91e-c7618c15399e", + "name": "analogInputChannel2", + "displayName": "Analog Channel 2", + "displayNameEvent": "Analog channel 2 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "de9172e5-b1c1-4a7e-aebe-819ef22c2c5a", + "name": "analogInputChannel3", + "displayName": "Analog Channel 3", + "displayNameEvent": "Analog channel 3 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "875ffbf1-3f11-4935-bd16-de983585baad", + "name": "analogInputChannel4", + "displayName": "Analog Channel 4", + "displayNameEvent": "Analog channel 4 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "fa97d343-544d-4322-aacd-4f9f7c21224b", + "name": "analogInputChannel5", + "displayName": "Analog Channel 5", + "displayNameEvent": "Analog channel 5 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "51d50bf9-6d40-4d63-a3a2-a0a7c0b5850d", + "name": "analogInputChannel6", + "displayName": "Analog Channel 6", + "displayNameEvent": "Analog channel 6 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "25ecf6a9-2007-4768-93a4-28ec81bac179", + "name": "analogInputChannel7", + "displayName": "Analog Channel 7", + "displayNameEvent": "Analog channel 7 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "93198a69-4ab6-4fd5-90ab-7f26a67b3b4a", + "name": "analogInputChannel8", + "displayName": "Analog Channel 8", + "displayNameEvent": "Analog channel 8 value changed", + "ioType": "analogInput", + "type": "double", + "minValue": 0, + "maxValue": 1, + "defaultValue": 0 + }, + { + "id": "e06c88f4-38d9-42cd-bab9-2a07a9a83ce0", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "bool", + "defaultValue": false + } + ] + } + ] + } + ] +} + + + diff --git a/usbrly82/meta.json b/usbrly82/meta.json new file mode 100644 index 00000000..d928b015 --- /dev/null +++ b/usbrly82/meta.json @@ -0,0 +1,13 @@ +{ + "title": "USB-RLY82", + "tagline": "Control relays connected over USB and read digital and analog inputs.", + "icon": "robotelectronics.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "usb" + ], + "categories": [ + "diy" + ] +} diff --git a/usbrly82/robotelectronics.png b/usbrly82/robotelectronics.png new file mode 100644 index 0000000000000000000000000000000000000000..b53c7adbf4d42a1c609e6885d72bd047f8011fa6 GIT binary patch literal 11898 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 literal 0 HcmV?d00001 diff --git a/usbrly82/serialportmonitor.cpp b/usbrly82/serialportmonitor.cpp new file mode 100644 index 00000000..3649795a --- /dev/null +++ b/usbrly82/serialportmonitor.cpp @@ -0,0 +1,190 @@ +#include "serialportmonitor.h" +#include "extern-plugininfo.h" + +#include + +SerialPortMonitor::SerialPortMonitor(QObject *parent) : QObject(parent) +{ + m_udev = udev_new(); + if (!m_udev) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Could not initialize udev"; + return; + } + + // Read initially all tty devices + struct udev_enumerate *enumerate = udev_enumerate_new(m_udev); + if (!enumerate) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Could not create udev enumerate for initial device reading."; + udev_unref(m_udev); + m_udev = nullptr; + return; + } + + // We are only interested in FTDI devices + udev_enumerate_add_match_subsystem(enumerate, "tty"); +// udev_enumerate_add_match_property(enumerate, "ID_VENDOR_ID", "04d8"); +// udev_enumerate_add_match_property(enumerate, "ID_MODEL_ID", "ffee"); + + if (udev_enumerate_scan_devices(enumerate) < 0) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Failed to scan devices from udev enumerate."; + udev_enumerate_unref(enumerate); + enumerate = nullptr; + udev_unref(m_udev); + m_udev = nullptr; + return; + } + + qCDebug(dcUsbRly82()) << "SerialPortMonitor: Load initial list of available serial ports..."; + struct udev_list_entry *devices = nullptr; + devices = udev_enumerate_get_list_entry(enumerate); + struct udev_list_entry *dev_list_entry = nullptr; + udev_list_entry_foreach(dev_list_entry, devices) { + struct udev_device *device = nullptr; + const char *path; + path = udev_list_entry_get_name(dev_list_entry); + device = udev_device_new_from_syspath(m_udev, path); + + // Print properties + struct udev_list_entry *properties = udev_device_get_properties_list_entry(device); + struct udev_list_entry *property_list_entry = nullptr; + udev_list_entry_foreach(property_list_entry, properties) { + qCDebug(dcUsbRly82()) << "SerialPortMonitor: - Property" << udev_list_entry_get_name(property_list_entry) << udev_list_entry_get_value(property_list_entry); + } + + QString vendorIdString = QString::fromLatin1(udev_device_get_property_value(device, "ID_VENDOR_ID")); + QString productIdString = QString::fromLatin1(udev_device_get_property_value(device, "ID_MODEL_ID")); + + SerialPortInfo info; + info.systemLocation = QString::fromLatin1(udev_device_get_property_value(device,"DEVNAME")); + info.manufacturer = QString::fromLatin1(udev_device_get_property_value(device,"ID_VENDOR_ENC")); + info.product = QString::fromLatin1(udev_device_get_property_value(device,"ID_MODEL_ENC")); + info.serialNumber = QString::fromLatin1(udev_device_get_property_value(device, "ID_SERIAL_SHORT")); + info.vendorId = static_cast(vendorIdString.toUInt(nullptr, 16)); + info.productId = static_cast(productIdString.toUInt(nullptr, 16)); + + // Clean up this device since we have all information + udev_device_unref(device); + + qCDebug(dcUsbRly82()) << "SerialPortMonitor: [+]" << info; + m_serialPortInfos.insert(info.systemLocation, info); + emit serialPortAdded(info); + } + + udev_enumerate_unref(enumerate); + enumerate = nullptr; + + // Create udev monitor + m_monitor = udev_monitor_new_from_netlink(m_udev, "udev"); + if (!m_monitor) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Could not initialize udev monitor."; + udev_unref(m_udev); + m_udev = nullptr; + return; + } + + // Set monitor filter to tty subsystem + if (udev_monitor_filter_add_match_subsystem_devtype(m_monitor, "tty", nullptr) < 0) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Could not set subsystem device type filter to tty."; + udev_monitor_unref(m_monitor); + m_monitor = nullptr; + udev_unref(m_udev); + m_udev = nullptr; + return; + } + + // Enable the monitor + if (udev_monitor_enable_receiving(m_monitor) < 0) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Could not enable udev monitor."; + udev_monitor_unref(m_monitor); + m_monitor = nullptr; + udev_unref(m_udev); + m_udev = nullptr; + return; + } + + // Create socket notifier for read + int socketDescriptor = udev_monitor_get_fd(m_monitor); + m_notifier = new QSocketNotifier(socketDescriptor, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, [this, socketDescriptor](int socket){ + + if (socketDescriptor != socket) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: socket != socketdescriptor"; + return; + } + + // Create udev device + udev_device *device = udev_monitor_receive_device(m_monitor); + if (!device) { + qCWarning(dcUsbRly82()) << "SerialPortMonitor: Got socket sotification but could not read device information."; + return; + } + + QString actionString = QString::fromUtf8(udev_device_get_action(device)); + + QString vendorIdString = QString::fromUtf8(udev_device_get_property_value(device, "ID_VENDOR_ID")); + QString productIdString = QString::fromUtf8(udev_device_get_property_value(device, "ID_MODEL_ID")); + + SerialPortInfo info; + info.systemLocation = QString::fromUtf8(udev_device_get_property_value(device,"DEVNAME")); + info.manufacturer = QString::fromUtf8(udev_device_get_property_value(device,"ID_VENDOR_ENC")); + info.product = QString::fromUtf8(udev_device_get_property_value(device,"ID_MODEL_ENC")); + info.serialNumber = QString::fromUtf8(udev_device_get_property_value(device, "ID_SERIAL_SHORT")); + info.vendorId = static_cast(vendorIdString.toUInt(nullptr, 16)); + info.productId = static_cast(productIdString.toUInt(nullptr, 16)); + + // Clean udev device + udev_device_unref(device); + + // Make sure we know the action + if (actionString.isEmpty()) + return; + + if (actionString == "add") { + qCDebug(dcUsbRly82()) << "SerialPortMonitor: [+]" << info; + if (!m_serialPortInfos.contains(info.systemLocation)) { + m_serialPortInfos.insert(info.systemLocation, info); + emit serialPortAdded(info); + } + } + + if (actionString == "remove") { + qCDebug(dcUsbRly82()) << "SerialPortMonitor: [-]" << info; + + if (m_serialPortInfos.contains(info.systemLocation)) { + m_serialPortInfos.remove(info.systemLocation); + emit serialPortRemoved(info); + } + } + }); + + m_notifier->setEnabled(true); +} + +SerialPortMonitor::~SerialPortMonitor() +{ + if (m_notifier) + delete m_notifier; + + if (m_monitor) + udev_monitor_unref(m_monitor); + + if (m_udev) + udev_unref(m_udev); + +} + +QList SerialPortMonitor::serialPortInfos() const +{ + return m_serialPortInfos.values(); +} + +QDebug operator<<(QDebug dbg, const SerialPortMonitor::SerialPortInfo &serialPortInfo) +{ + dbg.nospace().noquote() << "SerialPort(" << QString("%1:%2") + .arg(serialPortInfo.vendorId, 4, 16, QLatin1Char('0')) + .arg(serialPortInfo.productId, 4, 16, QLatin1Char('0')) + << ", " << serialPortInfo.systemLocation + << ", " << serialPortInfo.manufacturer + << ", " << serialPortInfo.product << ") "; + return dbg.maybeSpace().maybeQuote(); +} diff --git a/usbrly82/serialportmonitor.h b/usbrly82/serialportmonitor.h new file mode 100644 index 00000000..2737cadf --- /dev/null +++ b/usbrly82/serialportmonitor.h @@ -0,0 +1,45 @@ +#ifndef SERIALPORTMONITOR_H +#define SERIALPORTMONITOR_H + +#include +#include +#include +#include + +#include + +class SerialPortMonitor : public QObject +{ + Q_OBJECT +public: + typedef struct SerialPortInfo { + QString manufacturer; + QString product; + QString serialNumber; + QString systemLocation; + quint16 vendorId; + quint16 productId; + } SerialPortInfo; + + explicit SerialPortMonitor(QObject *parent = nullptr); + ~SerialPortMonitor(); + + QList serialPortInfos() const; + +signals: + void serialPortAdded(const SerialPortInfo &serialPortInfo); + void serialPortRemoved(const SerialPortInfo &serialPortInfo); + +private: + struct udev *m_udev = nullptr; + struct udev_monitor *m_monitor = nullptr; + QSocketNotifier *m_notifier = nullptr; + + QHash m_serialPortInfos; + +}; + +QDebug operator<< (QDebug dbg, const SerialPortMonitor::SerialPortInfo &serialPortInfo); + + +#endif // SERIALPORTMONITOR_H diff --git a/usbrly82/usbrly82.cpp b/usbrly82/usbrly82.cpp new file mode 100644 index 00000000..0d06de1f --- /dev/null +++ b/usbrly82/usbrly82.cpp @@ -0,0 +1,333 @@ +#include "usbrly82.h" +#include "extern-plugininfo.h" + + +UsbRly82Reply::Error UsbRly82Reply::error() const +{ + return m_error; +} + +QByteArray UsbRly82Reply::requestData() const +{ + return m_requestData; +} + +QByteArray UsbRly82Reply::responseData() const +{ + return m_responseData; +} + +UsbRly82Reply::UsbRly82Reply(QObject *parent) : QObject(parent) +{ + m_timer.setSingleShot(true); + m_timer.setInterval(1000); + connect(&m_timer, &QTimer::timeout, this, [this](){ + m_error = ErrorTimeout; + emit finished(); + }); +} + + + +UsbRly82::UsbRly82(QObject *parent) : QObject(parent) +{ + qRegisterMetaType(); + m_refreshTimer.setInterval(100); + m_refreshTimer.setSingleShot(false); + connect(&m_refreshTimer, &QTimer::timeout, this, &UsbRly82::poll); +} + +bool UsbRly82::available() const +{ + return m_available; +} + +bool UsbRly82::powerRelay1() const +{ + return m_powerRelay1; +} + +UsbRly82Reply *UsbRly82::setRelay1Power(bool power) +{ + UsbRly82Reply *reply; + if (power) { + reply = createReply(QByteArray::fromHex("65"), false); + } else { + reply = createReply(QByteArray::fromHex("6F"), false); + } + sendNextRequest(); + return reply; +} + + +bool UsbRly82::powerRelay2() const +{ + return m_powerRelay2; +} + +UsbRly82Reply *UsbRly82::setRelay2Power(bool power) +{ + UsbRly82Reply *reply; + if (power) { + reply = createReply(QByteArray::fromHex("66"), false); + } else { + reply = createReply(QByteArray::fromHex("70"), false); + } + reply->m_expectsResponse = false; + sendNextRequest(); + return reply; +} + +quint8 UsbRly82::digitalInputs() const +{ + return m_digitalInputs; +} + +bool UsbRly82::connectRelay(const QString &serialPort) +{ + qCDebug(dcUsbRly82()) << "Connecting to" << serialPort; + if (m_serialPort) { + m_serialPort->close(); + delete m_serialPort; + m_serialPort = nullptr; + } + + m_available = false; + + m_serialPort = new QSerialPort(serialPort, this); + m_serialPort->setBaudRate(19200); + m_serialPort->setStopBits(QSerialPort::OneStop); + m_serialPort->setParity(QSerialPort::NoParity); + + if (!m_serialPort->open(QIODevice::ReadWrite)) { + qCWarning(dcUsbRly82()) << "Could not open serial port" << serialPort << m_serialPort->errorString(); + m_serialPort->deleteLater(); + m_serialPort = nullptr; + emit availableChanged(m_available); + return false; + } + + connect(m_serialPort, &QSerialPort::readyRead, this, &UsbRly82::onReadyRead); + connect(m_serialPort, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(onError(QSerialPort::SerialPortError)), Qt::QueuedConnection); + + // Get serial number + UsbRly82Reply *reply = getSerialNumber(); + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + qCWarning(dcUsbRly82()) << "Reading serial number finished with error" << reply->error(); + return; + } + + m_serialNumber = QString::fromUtf8(reply->responseData()); + qCDebug(dcUsbRly82()) << "Get serial number finished successfully." << m_serialNumber; + + // Get software version + UsbRly82Reply *reply = getSoftwareVersion(); + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + qCWarning(dcUsbRly82()) << "Reading software version finished with error" << reply->error(); + return; + } + + m_softwareVersion = QString::fromUtf8(reply->responseData().toHex()); + qCDebug(dcUsbRly82()) << "Get software version finished successfully." << m_softwareVersion; + + UsbRly82Reply *reply = getRelayStates(); + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + qCWarning(dcUsbRly82()) << "Reading relay states finished with error" << reply->error(); + return; + } + + qCDebug(dcUsbRly82()) << "Reading relay states finished successfully." << reply->responseData().toHex(); + bool power = checkBit(reply->responseData().at(0), 0); + if (m_powerRelay1 != power) { + m_powerRelay1 = power; + emit powerRelay1Changed(m_powerRelay1); + } + + power = checkBit(reply->responseData().at(0), 1); + if (m_powerRelay2 != power) { + m_powerRelay2 = power; + emit powerRelay2Changed(m_powerRelay2); + } + + qCDebug(dcUsbRly82()) << "Relay 1:" << m_powerRelay1; + qCDebug(dcUsbRly82()) << "Relay 2:" << m_powerRelay2; + + m_available = true; + emit availableChanged(m_available); + + m_refreshTimer.start(); + }); + }); + }); + + return true; +} + +void UsbRly82::disconnectRelay() +{ + if (m_serialPort) { + qCDebug(dcUsbRly82()) << "Disconnecting from" << m_serialPort->portName(); + m_serialPort->close(); + delete m_serialPort; + m_serialPort = nullptr; + } + + m_refreshTimer.stop(); + + m_available = false; + emit availableChanged(m_available); +} + + +UsbRly82Reply *UsbRly82::getSerialNumber() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("38")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::getSoftwareVersion() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("5A")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::getRelayStates() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("5B")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::getDigitalInputs() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("5E")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::getAdcValues() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("80")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::getAdcReference() +{ + UsbRly82Reply *reply = createReply(QByteArray::fromHex("82")); + sendNextRequest(); + return reply; +} + +UsbRly82Reply *UsbRly82::createReply(const QByteArray &requestData, bool expectsResponse) +{ + UsbRly82Reply *reply = new UsbRly82Reply(this); + reply->m_expectsResponse = expectsResponse; + reply->m_requestData = requestData; + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (m_currentReply == reply) { + m_currentReply = nullptr; + sendNextRequest(); + } + + reply->deleteLater(); + }); + + if (!expectsResponse) { + m_replyQueue.enqueue(reply); + } else { + // Priorize requests without response (like switching the relay) + m_replyQueue.prepend(reply); + } + return reply; +} + +void UsbRly82::sendNextRequest() +{ + if (m_currentReply) + return; + + if (m_replyQueue.isEmpty()) + return; + + m_currentReply = m_replyQueue.dequeue(); + qCDebug(dcUsbRly82()) << "-->" << m_currentReply->requestData().toHex(); + m_serialPort->write(m_currentReply->requestData()); + if (m_currentReply->m_expectsResponse) { + m_currentReply->m_timer.start(1000); + } else { + // Finish the reply on next event loop + QTimer::singleShot(0, m_currentReply, &UsbRly82Reply::finished); + } +} + +bool UsbRly82::checkBit(quint8 byte, uint bitNumber) +{ + return ((byte >> bitNumber) & 0x01) == 1; +} + +void UsbRly82::onReadyRead() +{ + QByteArray data = m_serialPort->readAll(); + qCDebug(dcUsbRly82()) << "<--" << data.toHex(); + + if (m_currentReply) { + m_currentReply->m_responseData = data; + m_currentReply->m_timer.stop(); + emit m_currentReply->finished(); + } else { + qCWarning(dcUsbRly82()) << "Unexpected data received" << data.toHex(); + } +} + +void UsbRly82::onError(QSerialPort::SerialPortError error) +{ + if (error != QSerialPort::NoError && error != QSerialPort::OpenError && m_serialPort && m_serialPort->isOpen()) { + qCWarning(dcUsbRly82()) << "Serial port error occurred:" << error << m_serialPort->errorString() << "(Is open:" << m_serialPort->isOpen() << ")"; + + m_available = false; + emit availableChanged(available()); + + disconnectRelay(); + } +} + +void UsbRly82::poll() +{ + if (m_replyQueue.count() > 10) + return; + + UsbRly82Reply *reply = getDigitalInputs(); + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + qCWarning(dcUsbRly82()) << "Reading digital inputs finished with error" << reply->error(); + return; + } + + if (reply->responseData().isEmpty()) + return; + + quint8 digitalInputs = reply->responseData().at(0); + if (m_digitalInputs != digitalInputs) { + qCDebug(dcUsbRly82()) << "Digital inputs changed"; + m_digitalInputs = digitalInputs; + emit digitalInputsChanged(); + } + }); + + + reply = getAdcValues(); + connect(reply, &UsbRly82Reply::finished, this, [=](){ + if (reply->error() != UsbRly82Reply::ErrorNoError) { + qCWarning(dcUsbRly82()) << "Reading analog inputs finished with error" << reply->error(); + return; + } + + qCDebug(dcUsbRly82()) << "Analog inputs"; + }); +} diff --git a/usbrly82/usbrly82.h b/usbrly82/usbrly82.h new file mode 100644 index 00000000..8c241aa0 --- /dev/null +++ b/usbrly82/usbrly82.h @@ -0,0 +1,106 @@ +#ifndef USBRLY82_H +#define USBRLY82_H + +#include +#include +#include +#include + +class UsbRly82Reply : public QObject +{ + Q_OBJECT + + friend class UsbRly82; + +public: + enum Error { + ErrorNoError, + ErrorTimeout + }; + Q_ENUM(Error) + + Error error() const; + QByteArray requestData() const; + QByteArray responseData() const; + +signals: + void finished(); + +private: + explicit UsbRly82Reply(QObject *parent = nullptr); + + Error m_error = ErrorNoError; + QTimer m_timer; + bool m_expectsResponse = true; + + QByteArray m_requestData; + QByteArray m_responseData; +}; + +class UsbRly82 : public QObject +{ + Q_OBJECT +public: + explicit UsbRly82(QObject *parent = nullptr); + + bool available() const; + + bool powerRelay1() const; + UsbRly82Reply *setRelay1Power(bool power); + + bool powerRelay2() const; + UsbRly82Reply *setRelay2Power(bool power); + + quint8 digitalInputs() const; + + bool connectRelay(const QString &serialPort); + void disconnectRelay(); + + UsbRly82Reply *getSerialNumber(); + UsbRly82Reply *getSoftwareVersion(); + UsbRly82Reply *getRelayStates(); + + UsbRly82Reply *getDigitalInputs(); + UsbRly82Reply *getAdcValues(); + UsbRly82Reply *getAdcReference(); + + static bool checkBit(quint8 byte, uint bitNumber); + + +signals: + void availableChanged(bool available); + + void powerRelay1Changed(bool powerRelay1); + void powerRelay2Changed(bool powerRelay2); + + void digitalInputsChanged(); + +private: + QTimer m_refreshTimer; + QSerialPort *m_serialPort = nullptr; + bool m_available = false; + + QString m_serialNumber; + QString m_softwareVersion; + + bool m_powerRelay1 = false; + bool m_powerRelay2 = false; + + UsbRly82Reply *m_currentReply = nullptr; + QQueue m_replyQueue; + + UsbRly82Reply *createReply(const QByteArray &requestData, bool expectsResponse = true); + void sendNextRequest(); + + + quint8 m_digitalInputs = 0x00; + +private slots: + void onReadyRead(); + void onError(QSerialPort::SerialPortError error); + + void poll(); + +}; + +#endif // USBRLY82_H diff --git a/usbrly82/usbrly82.pro b/usbrly82/usbrly82.pro new file mode 100644 index 00000000..0c808803 --- /dev/null +++ b/usbrly82/usbrly82.pro @@ -0,0 +1,16 @@ +include(../plugins.pri) + +QT *= serialport + +PKGCONFIG += libudev + +HEADERS += \ + integrationpluginusbrly82.h \ + serialportmonitor.h \ + usbrly82.h + +SOURCES += \ + integrationpluginusbrly82.cpp \ + serialportmonitor.cpp \ + usbrly82.cpp +