abratchik / HIDPowerDevice

This project allows to use Arduino Leonardo or Arduino Pro Micro as an intelligent UPS controller.
137 stars 36 forks source link

Linux / rPi not working with Pro Micro 3.3V 8MHz #1

Open Chris2834 opened 3 years ago

Chris2834 commented 3 years ago

Positive 👍 Got it working on Pro Micro Clone with 3.3V @8MHz & 5V @16MHz

issue :

on linux system one has to add / change the VID & PID related to used clone / firmware For SparkFun Pro Micro 3.3V @8MHz is as follows :

ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9204", ENV{UPOWER_BATTERY_TYPE}="ups"

But still (Ubuntu 20) on rPI4 refuses to load usbhid-lib for NUT support.

Double checked all blacklist.rules and did find other vid/pid to be blocked, but not this one.

Any idea ? Did anyone get it running with NUT & usbhid-ups? Do I have to use another driver interface? Or other SW package ?

abratchik commented 3 years ago

I think it should work with usbhid-ups in theory as, on the lower level, this is just a USB HID Power Device interface specs implementation. May be the problem is in configuration of usbhid-ups as it picks up only specific models (vid/pid). I did not spend much time on it as the idea was to have the Arduino UPS working out-of-the-box on a vanilla system (Mac, Win or Linux), without deploying any additional packages like NUT.

Chris2834 commented 3 years ago

Fully agree, runs out of the box with "one" local machine as intended out-of-the-box on Mac, Win and Linux ! Thanks for that !

But .. can not get it working on Raspberry or QNAP NAS directly - both to monitor and react on UPS message.

Even managing with vid/pid in ups.conf and rules.d is not getting the job done. So even single vid/pid combination restrictions (checked Leonardo and SparkFun 3.3V) does not get usbhid-ups included in NUT working.

If it would work on QNAP NAS as HID device - so my basic idea - it would be monitored for other devices in IP range by QNAP via NUT msg protocol.

If anyone has an idea ? More than welcome.

logancarlile97 commented 3 years ago

I am having the same problem. From what I am seeing it might require creating a custom sub driver for the usbhid-ups driver in NUT. https://networkupstools.org/docs/developer-guide.chunked/ar01s04.html#hid-subdrivers

logancarlile97 commented 3 years ago

After some further digging if found that the usbhid-ups driver for NUT reports this when trying to connect to the arduino:

0.174690 Checking device (2341/8036) (002/018) 0.177223 - VendorID: 2341 0.177265 - ProductID: 8036 0.177287 - Manufacturer: Arduino LLC 0.177298 - Product: Arduino Leonardo 0.177319 - Serial Number: UPS10 0.177336 - Bus: 002 0.177355 - Device release number: 0100 0.177374 Trying to match device 0.177422 Device matches 0.177448 failed to claim USB device: could not claim interface 0: Device or resource busy 0.177835 detached kernel driver from USB device... 0.177896 nut_usb_set_altinterface: skipped usb_set_altinterface(udev, 0) 0.178103 Unable to get HID descriptor (error sending control message: Broken pipe) 0.178162 HID descriptor length (method 1) -1 0.178176 HID descriptor length (method 2) -1 0.178188 Unable to retrieve any HID descriptor

Does anyone know why the driver would not be able to retrieve the HID descriptor? If we could get this working with NUT that would be amazing!

abratchik commented 3 years ago

Well I guess the magic happens (or doesn't happen) in here where the NUT is trying to retrieve the HID descriptor for the UPS:

            /* Get HID descriptor */

            /* FIRST METHOD: ask for HID descriptor directly. */
            /* res = usb_get_descriptor(udev, USB_DT_HID, hid_desc_index, buf, 0x9); */
            res = usb_control_msg(udev, USB_ENDPOINT_IN+1, USB_REQ_GET_DESCRIPTOR,
                    (USB_DT_HID << 8) + hid_desc_index, 0, buf, 0x9, USB_TIMEOUT);

            if (res < 0) {
                upsdebugx(2, "Unable to get HID descriptor (%s)", usb_strerror());
            } else if (res < 9) {
                upsdebugx(2, "HID descriptor too short (expected %d, got %d)", 8, res);
            } else {

                upsdebug_hex(3, "HID descriptor, method 1", buf, 9);

                rdlen1 = buf[7] | (buf[8] << 8);
            }

            if (rdlen1 < -1) {
                upsdebugx(2, "Warning: HID descriptor, method 1 failed");
            }
            upsdebugx(3, "HID descriptor length (method 1) %d", rdlen1);

First it tries to send the control message to get the descriptor to the wrong pipe. I believe this is because they assume the 1st interface (Interface 0) in the device descriptor to be the one they need to talk to. Unfortunately, this is not the case - Arduino Leonardo/Micro reports itself through USB as a composite device and uses the Interface 2 for all HID projects, including HID UPS. Hence the "Broken Pipe" message.

Their second method of retrieving the correct descriptor is very special and is to cater cases of "broken" UPS like Tripp lite Smart1000 LCD - they search for HID descriptor in the "extra" bytes of the Interface, while (wrongly) assuming the Interface 0 as the one they need to look at.

            /* SECOND METHOD: find HID descriptor among "extra" bytes of
               interface descriptor, i.e., bytes tucked onto the end of
               descriptor 2. */

            /* Note: on some broken UPS's (e.g. Tripp Lite Smart1000LCD),
                only this second method gives the correct result */

            /* for now, we always assume configuration 0, interface 0,
               altsetting 0, as above. */
            iface = &dev->config[0].interface[0].altsetting[0];
            for (i=0; i<iface->extralen; i+=iface->extra[i]) {
                upsdebugx(4, "i=%d, extra[i]=%02x, extra[i+1]=%02x", i,
                    iface->extra[i], iface->extra[i+1]);
                if (i+9 <= iface->extralen && iface->extra[i] >= 9 && iface->extra[i+1] == 0x21) {
                    p = &iface->extra[i];
                    upsdebug_hex(3, "HID descriptor, method 2", p, 9);
                    rdlen2 = p[7] | (p[8] << 8);
                    break;
                }
            }

I guess making NUT to work with Arduino UPS requires fixing of the libusb. This whole piece of code there is quite ugly - they hardcode VID/PID for Eaton, hardcode interface numbers etc. May be, too many people hacked it quick and dirty for specific UPS models.

logancarlile97 commented 3 years ago

Ok, so I found a parameter called usb_set_altinterface. According to the documentation this should allow me to change the USB interface that NUT is using. However, when I set this to interface 1 or interface 2 I get this output.

   0.173726    Checking device (2341/8036) (002/004)
   0.175940 - VendorID: 2341
   0.175988 - ProductID: 8036
   0.176028 - Manufacturer: Arduino LLC
   0.176056 - Product: Arduino Leonardo
   0.176079 - Serial Number: UPS10
   0.176103 - Bus: 002
   0.176127 - Device release number: 0100
   0.176145 Trying to match device
   0.176225 Device matches
   0.176313 nut_usb_set_altinterface: calling usb_set_altinterface(udev, 1)
   0.176386 nut_usb_set_altinterface: usb_set_altinterface(udev, 1) returned -22 (could not set alt intf 0/1: Invalid argument)
   0.176420 nut_usb_set_altinterface: usb_set_altinterface() should not be necessary - please email the nut-upsdev list with information about your UPS.
   0.176688 Unable to get HID descriptor (error sending control message: Broken pipe)
   0.176760 HID descriptor length (method 1) -1
   0.176797 i=0, extra[i]=05, extra[i+1]=24
   0.176823 i=5, extra[i]=05, extra[i+1]=24
   0.176849 i=10, extra[i]=04, extra[i+1]=24
   0.176875 i=14, extra[i]=05, extra[i+1]=24
   0.176900 HID descriptor length (method 2) -1

Could this be a problem with NUT or am I just getting things wrong

logancarlile97 commented 3 years ago

This is the related line of code in libusb

/*! If needed, set the USB alternate interface.
 *
 * In NUT 2.7.2 and earlier, the following call was made unconditionally:
 *   usb_set_altinterface(udev, 0);
 *
 * Although harmless on Linux and *BSD, this extra call prevents old Tripp Lite
 * devices from working on Mac OS X (presumably the OS is already setting
 * altinterface to 0).
 */
static int nut_usb_set_altinterface(usb_dev_handle *udev)
{
    int altinterface = 0, ret = 0;
    char *alt_string, *endp = NULL;

    if(testvar("usb_set_altinterface")) {
        alt_string = getval("usb_set_altinterface");
        if(alt_string) {
            altinterface = (int)strtol(alt_string, &endp, 10);
            if(endp && !(endp[0] == 0)) {
                upslogx(LOG_WARNING, "%s: '%s' is not a valid number", __func__, alt_string);
            }
            if(altinterface < 0 || altinterface > 255) {
                upslogx(LOG_WARNING, "%s: setting bAlternateInterface to %d will probably not work", __func__, altinterface);
            }
        }
        /* set default interface */
        upsdebugx(2, "%s: calling usb_set_altinterface(udev, %d)", __func__, altinterface);
        ret = usb_set_altinterface(udev, altinterface);
        if(ret != 0) {
            upslogx(LOG_WARNING, "%s: usb_set_altinterface(udev, %d) returned %d (%s)",
                    __func__, altinterface, ret, usb_strerror() );
        }
        upslogx(LOG_NOTICE, "%s: usb_set_altinterface() should not be necessary - please email the nut-upsdev list with information about your UPS.", __func__);
    } else {
        upsdebugx(3, "%s: skipped usb_set_altinterface(udev, 0)", __func__);
    }
    return ret;
}
abratchik commented 3 years ago

I do not think the altinterface will help. As far as I understand, this is an alternative set of parameters, which can be defined for an interface (Interface 0 in this case). You may check here for more info about this concept.

However, we need to make the NUT using the different interface number - Interface 2 in case of Arduino HID. Based on what I see in the code of usbhid-ups, I do not believe it is possible to configure this without forking NUT.

logancarlile97 commented 3 years ago

Thank you for the article. Is there a way we could swap the interface numbers so that HID projects use interface 0 rather than 2. Forgive me if this is a dumb question I am new to all of this.

logancarlile97 commented 3 years ago

I managed to change the interface number of CDC ACM and CDC Data by changing two definitions in the USBDesc.h file for the Arduino IDE. However, I can't figure out how to change the HID interface number.

Original:

#define CDC_ACM_INTERFACE   0   // CDC ACM
#define CDC_DATA_INTERFACE  1   // CDC Data

New:

#define CDC_ACM_INTERFACE  3   // CDC ACM
#define CDC_DATA_INTERFACE  4   // CDC Data

lsusb -v output

Original:

Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass          239 Miscellaneous Device
  bDeviceSubClass         2 
  bDeviceProtocol         1 Interface Association
  bMaxPacketSize0        64
  idVendor           0x2341 Arduino SA
  idProduct          0x8036 Leonardo (CDC ACM, HID)
  bcdDevice            1.00
  iManufacturer           1 
  iProduct                2 
  iSerial                 3 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x006b
    bNumInterfaces          3
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              500mA
    Interface Association:
      bLength                 8
      bDescriptorType        11
      bFirstInterface         0
      bInterfaceCount         2
      bFunctionClass          2 Communications
      bFunctionSubClass       2 Abstract (modem)
      bFunctionProtocol       0 
      iFunction               0 
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         2 Communications
      bInterfaceSubClass      2 Abstract (modem)
      bInterfaceProtocol      0 
      iInterface              0 
      CDC Header:
        bcdCDC               1.10
      CDC Call Management:
        bmCapabilities       0x01
          call management
        bDataInterface          1
      CDC ACM:
        bmCapabilities       0x06
          sends break
          line coding and serial state
      CDC Union:
        bMasterInterface        3
        bSlaveInterface         4 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0010  1x 16 bytes
        bInterval              64
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass        10 CDC Data
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              0 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x02  EP 2 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x83  EP 3 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        5
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.01
          bCountryCode           33 US
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength     400

New:

Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass          239 Miscellaneous Device
  bDeviceSubClass         2 
  bDeviceProtocol         1 Interface Association
  bMaxPacketSize0        64
  idVendor           0x2341 Arduino SA
  idProduct          0x8036 Leonardo (CDC ACM, HID)
  bcdDevice            1.00
  iManufacturer           1 
  iProduct                2 
  iSerial                 3 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x006b
    bNumInterfaces          3
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              500mA
    Interface Association:
      bLength                 8
      bDescriptorType        11
      bFirstInterface         0
      bInterfaceCount         2
      bFunctionClass          2 Communications
      bFunctionSubClass       2 Abstract (modem)
      bFunctionProtocol       0 
      iFunction               0 
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        3
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         2 Communications
      bInterfaceSubClass      2 Abstract (modem)
      bInterfaceProtocol      0 
      iInterface              0 
      CDC Header:
        bcdCDC               1.10
      CDC Call Management:
        bmCapabilities       0x01
          call management
        bDataInterface          1
      CDC ACM:
        bmCapabilities       0x06
          sends break
          line coding and serial state
      CDC Union:
        bMasterInterface        3
        bSlaveInterface         4 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0010  1x 16 bytes
        bInterval              64
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        4
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass        10 CDC Data
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              0 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x02  EP 2 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x83  EP 3 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        5
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.01
          bCountryCode           33 US
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength     400
abratchik commented 3 years ago

I think manipulating with CDC interfaces is not very good idea as it may brick your Arduino board so that you will need to re-flash the boot loader to make it programmable again. I still believe the issue needs to be fixed in NUT because it is NUT not following USB specification, not the Arduino. They assume that the UPS is always reporting itself in the Interface 0 but it is not correct by USB protocol. In theory, you may build a composite device, which works as UPS and, let's say, printer so you cannot really rely on the UPS being always the 1st interface. That is why all the OOB HID UPS drivers, except NUT and usbhid-ups, work in all systems with Arduino UPS.

However, if you still want to try make the HID UPS the first interface, you may look at PluggableUSB module. As far as I understand, the HID interface number is assigned in the PluggableUSB_::plug() method as num of interfaces + 1:

    ...
        node->pluggedInterface = lastIf;
    node->pluggedEndpoint = lastEp;
    lastIf += node->numInterfaces;
    for (uint8_t i = 0; i < node->numEndpoints; i++) {
        _initEndpoints[lastEp] = node->endpointType[i];
        lastEp++;
    }
    ...

That is why HID is coming always as a 3rd interface after CDC. I guess you may tweak the Arduino USBCore and remove the CDC completely - this way you will get HID as your first interface. After that, you will need to re-flash the boot-loader if you want to re-program as the Arduino IDE wont be able to connect to the board without CDC.

shadow578 commented 3 years ago

Managed to get this working with NUT (on TrueNas SCALE, tho it should work on any other os too). Quick note that this solution is really just multiple hacks, but hey, if it works it ain't stupid.

  1. you have to disable CDC so the HID device is the only descriptor on the arduino. Luckily, the AVR Cores have a pull request implementing exactly this, so you just have to kinda copy that. It may also be possible to keep CDC as the second / third descriptor, but just disabling it is way simpler.

  2. since we now no longer have Serial, all calls to Serial.* have to be removed / commented in HIDPowerDevice.ino

  3. lastly, NUT for some reason does not detect the device with stock PID and VID, so you have to change it. This can be done in the boards.txt file, keys leonardo.build.vid and leonardo.build.pid. I just values equivalent to a EATON E51100iUSB, tho any random ups that supports the usbhid-ups driver should work fine for this.

  4. upload the sketch. This has to be done using a ICSP (like a USBASP) as we disabled CDC. Well, technically you could upload using the "normal" method, however only the first time.

The NUT config file looks something like this:

[ups]
  driver = usbhid-ups
  port = auto
  desc = Arduino UPS  // optional
  pollfreq = 30             // optional
  vendorid = 0463       // same as in boards.txt
  productid = ffff         // same as in boards.txt

(pollfreq, vendorid and productid can be added in "Auxiliary Parameters (ups.conf)" field in TrueNas)

From here on, it's just like with any other UPS. Note, however, that this solution really is just a bunch of hacks to the arduino core, and may break any time. That being said, I'd also recommend reverting all changed files to their original state to avoid unpleasant surprises with other projects.

abratchik commented 3 years ago

@shadow578 wow, this is impressive work, thank you. I think this is a very valid approach if a specific model of UPS need to be simulated for testing purposes. Agree with you that the solution is looking custom but this is just a consequence of hacks in nut / libusb. The code there is ugly, that is why it is hard to extend and use with the generic UPS.

Chris2834 commented 3 years ago

@shadow578 ! Nicely done. Thanks for the update. Although I got it running on Ubuntu 20.x with the PID changes posted, I will also give this try.

abratchik commented 3 years ago

I ended up forking NUT as this seems to be more correct way of fixing this issue. You may try to build nut build from here - https://github.com/abratchik/nut. It should be compatible with Arduino UPS sketches out of the box.

Also created PR to NUT so there is a (small) chance that they merge this change and it will arrive to Synology boxes naturally, along with system upgrades.

jimklimov commented 3 years ago

Thank you all for this investigation and PR. As for "natural arrival", we in NUT project have some loose threads to catch up with before making a satisfactory release, so sadly cutting an official release number gets delayed time and again. But indeed, when that happens, distributions should have it disseminated and so have new drivers, features and fixes available to everybody.

dosmenbsd commented 2 years ago

I was able to successfully connect arduino Leonardo to my Synology DS220+ (DSM 7.1.1-42962 Update 2)

  1. There were many attempts, I had to restore bootloader many times. Must have a USBasp (or other compatible from arduino IDE). Following actions make it impossible to flash Leonardo without of external programmer.

  2. Modify file boards.txt

leonardo.build.mcu=atmega32u4
leonardo.build.f_cpu=16000000L
leonardo.build.vid=0x0D9F
leonardo.build.pid=0x0004
leonardo.build.usb_product="HID UPS Battery"
leonardo.build.board=AVR_LEONARDO
leonardo.build.core=arduino
leonardo.build.variant=leonardo
leonardo.build.extra_flags={build.usb_flags}
  1. Modify file USBDesc.h #define CDC_DISABLED

Thank you!

abratchik commented 2 years ago

Hi @dosmenbsd , great effort, thanks for sharing!

BTW, the NUT library fix to support UPS with the USB Interface > 1 has been merged already to the master, many thanks to @jimklimov . So hopefully it will be picked up by Sinology and other distros in the future, to ensure better compatibility, and support of Arduino UPS as a side effect, without losing the CDC.

jimklimov commented 2 years ago

Reading up into the thread, I see experiments setting other vendors' VID/PID. I understand it can help in proof-of-concepts to get at least something running, but since few vendors follow specs precisely and exactly (and/or particular firmware releases are problematic), some drivers happen to have "if ID then" quirks in the middle of the codebase that helped someone sometime.

If there is (a possibility to get) an Arduino UPS specific set of IDs, we can code those into the drivers ("this NUT driver is good for these VID:PID pairs: M:N, ... , X:Z"), it may be much more reliable for integrations like this in the long run.

abratchik commented 2 years ago

Hi @jimklimov, thank you for your feedback! Most of Arduino boards equipped with the USB port are here.

jimklimov commented 2 years ago

Thanks for the link, but so far I could only look cursorily (commuting). Are there two VIDs in play (0x2341 and 0x2A03), and a few PIDs? NUT-wise can all of these, or only some, be assumed good fits for usbhid-ups handling?

jimklimov commented 2 years ago

Notably, those ones are already listed at https://github.com/networkupstools/nut/blob/a6ad5c9463a404b02a686614cd83d267fe0abc85/drivers/arduino-hid.c#L38-L55 so might "just work" out of the box

abratchik commented 2 years ago

The function of the Arduino "perceived" by the host is entirely dependent on the HID descriptor, which is uploaded to the Arduino board along with the sketch code. In theory, the same VID/PID can be used for a custom keyboard, a mouse or any other HID device. Ideally the usbhid-ups should only rely on HID Power Device specs and HID descriptor, and not on VID/PID at all.

In some Linux distros (Ubuntu, in example), there is an (obsolete) concept in udev manager, which relies on VID/PID for determining device family and purpose. If some VID/PID are not registered properly, they will be ignored by the udev. The workaround is manual registration, through the config files, but it is not documented very well. It took quite a lot of time for me to understand why Windows and OSX could "see" the Arduino UPS out of the box while Ubuntu was ignoring it completely.

jimklimov commented 2 years ago

Reluctantly agree - in a perfect world where everyone followed the specs and did not introduce extensions, this would work :)

One "issue" with this as regards NUT in particular is that there are many non-HID devices connected by USB. Most others are effectively serial-over-USB variants of Megatec QX protocol family, though some others exist too. Matching by VID:PID mostly allows to rule out which drivers to not try (except for completely sloppy vendors who think they can get away with IDs like 0000, 0001, ffff and a random protocol behind that). Similarly, this allows nut-scanner to suggest a suitable driver during discovery.

Another issue with drivers for vendors that follow some standard (USB HID, SNMP, Megatec QX...) is that everyone extends it. Often for a good reason: common standards tend to be the lowest common denominator, like "I'm a battery and I'm online", so ignoring extensible nuances like number of manageable outputs and their power-metering counters that some devices have and others do not. So for every generic "family" driver we end up with a lot of subdrivers that map vendor-specific extensions into a common framework. And generally just relying on "this is an USB HID UPS" without caring for further detail (usually guessed per VID:PID) would only see/manage trivial info even if it can in fact do more.

Yet another issue that we woe with lately is that many vendors do not care to implement even the basic USB protocol correctly. We have lots of of issues tagged https://github.com/networkupstools/nut/labels/USB-HID%20encoding%2FLogMin%2FLogMax for cases where even big-name vendors botched the numbers encoding. So knowing who is who helps apply "reasonable hotfixes" to the bit stream.

Creo2005 commented 1 year ago

And you can do it better, I didn’t quite understand what to fix and where exactly, and which file to flash? I want to connect UPS arduino to Synology NAS 218+ from this instruction https://projecthub.arduino.cc/abratchik/3e597a31-9844-4981-bede-fdac7944ad71#section5

B0urdonnais commented 1 year ago

I went through the interesting process of building Network Ups Tool (configure part was interesting) and got the arduino detected within ubuntu:

# upsc myups
device.mfr: Arduino LLC
device.model: Arduino Leonardo
device.serial: UPS10
device.type: ups
driver.name: usbhid-ups
driver.parameter.pollfreq: 10
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.productid: 8036
driver.parameter.synchronous: auto
driver.parameter.vendorid: 2341
driver.state: quiet
driver.version: 2.8.0-Windows-542-gccfb3569a
driver.version.data: Arduino HID 0.2
driver.version.internal: 0.49
driver.version.usb: libusb-0.1 (or compat)
ups.delay.shutdown: 20
ups.mfr: Arduino LLC
ups.model: Arduino Leonardo
ups.productid: 8036
ups.serial: UPS10
ups.status: OB
ups.timer.reboot: -1
ups.timer.shutdown: -1
ups.vendorid: 2341

The thing is that i'm missing some parameters, like voltage and remaining%, and I can't find a way to change the "OB" (OnBattery) status.

I think have the right usb behavior:

# udevadm info /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1
P: /devices/pci0000:00/0000:00:14.0/usb1/1-1
N: bus/usb/001/012
L: 0
E: DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1
E: DEVNAME=/dev/bus/usb/001/012
E: DEVTYPE=usb_device
E: DRIVER=usb
E: PRODUCT=2341/8036/100
E: TYPE=239/2/1
E: BUSNUM=001
E: DEVNUM=012
E: MAJOR=189
E: MINOR=11
E: SUBSYSTEM=usb
E: USEC_INITIALIZED=2609711065
E: ID_VENDOR=Arduino_LLC
E: ID_VENDOR_ENC=Arduino\x20LLC
E: ID_VENDOR_ID=2341
E: ID_MODEL=Arduino_Leonardo
E: ID_MODEL_ENC=Arduino\x20Leonardo
E: ID_MODEL_ID=8036
E: ID_REVISION=0100
E: ID_SERIAL=Arduino_LLC_Arduino_Leonardo_UPS10
E: ID_SERIAL_SHORT=UPS10
E: ID_BUS=usb
E: ID_USB_INTERFACES=:020200:0a0000:030000:
E: ID_VENDOR_FROM_DATABASE=Arduino SA
E: ID_MODEL_FROM_DATABASE=Leonardo (CDC ACM, HID)
E: ID_PATH=pci-0000:00:14.0-usb-0:1
E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_1
E: UPOWER_VENDOR=Arduino
E: UPOWER_BATTERY_TYPE=ups

(reported type=ups)

ups.conf is like so:

[myups]
  driver = usbhid-ups
  port = auto
  desc = Arduino UPS
  pollfreq = 2
  vendorid = 2341
  productid = 8036

edit, adding output from upsdrvctl -DD start

   0.000399     [D1] Starting UPS: myups
   0.000440     [D2] 10 remaining attempts
   0.000459     [D2] exec:  /lib/nut/usbhid-ups -a myups
Network UPS Tools - Generic HID driver 0.49 (2.8.0-Windows-542-gccfb3569a)
USB communication driver (libusb 0.1) 0.44
Using subdriver: Arduino HID 0.2

Anyone got a different output from upsc ?

Edit : I found out why nut did not pick the properties from the UPS : default driver collects a very reduced set of properties. I unsucessfully tried to "write" a nut subdriver but have not been able to make it be chosen instead of Arduino HID 0.2 in current NUT (2.8.0) build, so I edited the arduino-hid.c in the source.

Now trying to get values for current ( namely HID_PD_CURRENT ) to work, can't seem to get them collected by the usbhid driver....

kellybyrd commented 1 year ago

I've got my own sketch working with MacOS and Windows, but I can't get my Linux-based QNAP to recognize the Arduino as a UPS HID device.

So, I decided to just try and get this working on an RPi running Ubuntu 23.10 (I'm NUT v2.8.0 with the @abratchik's PR in it will help)

So far, I am failing to get NUT to recognize the Arduino. I haver tried both with CDC enabled and disabled (just doing #define CDC_DISABLED in USBSDesc.h) neither works.

Running /usr/lib/nut/usbhid-ups -DDDDDD -s test -x port=auto -x vendorid="2341" gets me:

Network UPS Tools - Generic HID driver 0.47 (2.8.0)
USB communication driver (libusb 1.0) 0.43
   0.000000     [D3] main_arg: var='port' val='auto'
   0.000146     [D5] send_to_all: SETINFO driver.parameter.port "auto"
   0.000198     [D3] main_arg: var='vendorid' val='2341'
   0.000252     [D5] send_to_all: SETINFO driver.parameter.vendorid "2341"
   0.000301     [D1] debug level is '6'
   0.008941     [D5] send_to_all: SETINFO device.type "ups"
   0.009036     [D2] Initializing an USB-connected UPS with library libusb-1.0.26 (API: 0x1000109) (NUT subdriver name='USB communication driver (libusb 1.0)' ver='0.43')
   0.009106     [D1] upsdrv_initups (non-SHUT)...
   0.043806     [D2] Checking device 1 of 5 (2341/8036)
   0.052844     [D2] - VendorID: 2341
   0.052946     [D2] - ProductID: 8036
   0.053009     [D2] - Manufacturer: Arduino LLC
   0.053075     [D2] - Product: Arduino Leonardo
   0.053138     [D2] - Serial Number: UPS11
   0.053200     [D2] - Bus: 001
   0.053267     [D2] - Device: unknown
   0.053352     [D2] - Device release number: 0100
   0.053419     [D2] Trying to match device
   0.053634     [D2] match_function_subdriver (non-SHUT mode): matching a device...
   0.053711     [D3] match_function_regex: matching a device...
   0.054176     [D2] Device matches
   0.054263     [D2] Reading first configuration descriptor
   0.054423     [D2] result: -5 (Entity not found)
   0.054518     [D3] libusb_kernel_driver_active() returned 1 (driver active)
   0.054582     [D2] successfully set kernel driver auto-detach flag
   0.056477     [D2] Claimed interface 2 successfully
   0.057463     [D3] nut_usb_set_altinterface: skipped libusb_set_interface_alt_setting(udev, 2, 0)
   0.057755     [D2]   Couldn't retrieve descriptors
   0.057989     [D2] Checking device 2 of 5 (0424/7800)
   0.058155     [D1] Failed to open device (0424/7800), skipping: Access denied (insufficient permissions)
   0.058236     [D2] Checking device 3 of 5 (0424/2514)
   0.058370     [D1] Failed to open device (0424/2514), skipping: Access denied (insufficient permissions)
   0.058507     [D2] Checking device 4 of 5 (0424/2514)
   0.058647     [D1] Failed to open device (0424/2514), skipping: Access denied (insufficient permissions)
   0.058729     [D2] Checking device 5 of 5 (1D6B/0002)
   0.058854     [D1] Failed to open device (1D6B/0002), skipping: Access denied (insufficient permissions)
   0.058924     [D2] libusb1: No appropriate HID device found
   0.058999     libusb1: Could not open any HID devices: insufficient permissions on everything
   0.059064     No matching HID UPS found

Any help is appreciated. Looking at the udev rules files that the NUT package installed (/lib/udev/rules.d/62-nut-usbups.rules) I see several lines intended for Arduino, including one that matches my VID/PID combo: ATTR{idVendor}=="2341", ATTR{idProduct}=="8036", MODE="664", GROUP="nut"

Any help is appreciated. Or maybe I should be posting to a NUT support channel?

kellybyrd commented 1 year ago

Re the above comment, I've traced the problem to I think a bug in NUT's libusb1. I'll report back here once I'm sure.

rik-shaw commented 12 months ago

Hi @dosmenbsd , great effort, thanks for sharing!

BTW, the NUT library fix to support UPS with the USB Interface > 1 has been merged already to the master, many thanks to @jimklimov . So hopefully it will be picked up by Sinology and other distros in the future, to ensure better compatibility, and support of Arduino UPS as a side effect, without losing the CDC.

@abratchik can you please confirm what the minimum version of nut is that has this fix incorporated?

kellybyrd commented 12 months ago

Re the above comment, I've traced the problem to I think a bug in NUT's libusb1. I'll report back here once I'm sure.

@abratchik can you please confirm what the minimum version of nut is that has this fix incorporated?

I can confirm NUT 2.8.0, not sure if 2.7.8 has it. Regardless, there is a bug in NUT's libusb1 (at least in v2.8.0, I think in 2.7.8). I submitted a PR for it and it had been merged, but that won't be available until NUT 2.8.2.

The 2.8.1 release happened on 10/31/23, and my PR was merged after that. I've seen talk that there's a problem with 2.8.1 so 2.8.2 may be released pretty soon. I don't know if it will include my PR or not, the team may decide to just patch the 2.8.1 release with the smallest possible fix for the (unrelated to my PR) problem.

The bug is that in libusb1's nut_libusb_open() function, the code was getting the device descriptor with

ret = libusb_get_config_descriptor(device,
            (uint8_t)usb_subdriver.hid_rep_index,
            &conf_desc);

This is wrong because hid_rep_index is the index of the HID power interface, but most USB devices have only a single config descriptor and it is at index 0. In the USB specs, interface descriptors belong to config descriptors, which belong to device descriptors. So on the rare devices with more than one config descriptor, config descriptor 0 may have 3 interfaces and config descriptor 1 may have 2 interfaces (or something else)

The end result for me is that for my Arduino, hid_rep_index was discovered to be 2, but the above code really needed to be:

ret = libusb_get_config_descriptor(device,
            (uint8_t)0
            &conf_desc);

Because my Arduino (like nearly all USB devices, AFAICT) has only a single config descriptor.

rik-shaw commented 12 months ago

I can confirm NUT 2.8.0, not sure if 2.7.8 has it.

@kellybyrd Thank you for the quick reply. I got 2.8.0 installed on Ubuntu 22.04, and wow now my Arduino Leonardo is seen correctly by nut! But now I am still slightly confused because previously I was able to see and interrogate the Leonardo using upower -e and get details by upower -i /org/freedesktop/UPower/devices/ups_hiddev0 (matching results from upower -e). But now upower -e does NOT report the Leonardo at all anymore! So NUT is happy but upower is not?

Since I hadn't found this thread until recently (the title didn't have me thinking it was relevant for NUT / Leonardo / Synology :-), I created issue #7 where I have a bit more info on my confusion. This treasure trove of info in Issue #1 is too broad!

I am not following your comments exactly about the bug in libusb1 that isn't yet patched, maybe upower now not detecting the device has something to do with that??

jimklimov commented 12 months ago

Just to clarify: not 2.7.8 but 2.7.4 (at least in upstream versioning) - this one was last of 2.7.x and released some 7 years ago. 2.8.0 was in Apr 22, and 2.8.1 a week ago (already with an errata though).

As for 2.8.2 - releases are slightly tidied up snapshots of master, so merged PRs would be there. There are no plans for one-off patch releases (hm, maybe we flex semver a bit too much), though distro packages are likely to add curated chains of patches over the release tarballs ;)

I believe @kellybyrd meant NUT's libusb1.c, an adapter to libusb-1.0 backend (as opposed to libusb0.c, libshut.c etc.), not the USB library project ;)

kellybyrd commented 10 months ago

Just following up with the success of getting this library working with NUT. It DOES WORK without having to remove CDC or play any tricks with usb vendor and product IDs.

I have a post-2.8.1 source tree of NUT running on a Raspberry Pi. I have this library, plus a heavily modified version of the example sketch in this repo (I added hardware to do proper AC mains on/off sensing as well as accurate DC battery voltage sensing and reporting. I'm relying on this setup to have my NAS (which connects over the network to the NUT server on the RPi) shutdown in various conditions.

Once NUT 2.8.2 is released, or whatever version has my fixes for NUT's libusb1, I'll try to get something compiled and running on my QNAP NAS directly. That may be more trouble than it is worth, though...