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 00000000..b53c7adb Binary files /dev/null and b/usbrly82/robotelectronics.png differ 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 +