3

I'm trying to develop a custom UPS using TinyUSB on Raspberry Pi Pico and getting it discovered by UPower on my Ubuntu host.

Currently my device is detected, but the host is not reading any information:

root@user-HP:~# upower -i /org/freedesktop/UPower/devices/ups_hiddev0
  native-path:          /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/usbmisc/hiddev0
  vendor:               APC
  model:                UPS
  serial:               123456
  power supply:         yes
  updated:              Sun, 02 Apr 2023 09:35:04 +0300 (16 seconds ago)
  has history:          yes
  has statistics:       yes
  ups
    present:             yes
    state:               empty
    warning-level:       none
    percentage:          0%
    icon-name:          'battery-empty-symbolic'

My expectation here is that state would be charging (see the test reports below).

Edit 5 Apr 2023: updated the report descriptor as per aja's suggestion

The device's report descriptor looks like this:

05 84 09 04 a1 01 09 10 a1 00 85 01 75 08 95 01 09 fd 79 01 b1 03 09 fe 79 02 b1 03 09 ff 79 03 b1 03 09 12 a1 00 85 02 05 85 75 01 95 04 17 00 00 00 00 27 01 00 00 00 09 44 09 45 09 46 09 47 81 82 95 01 75 04 81 01 c0 c0 c0 

  INPUT(2)[INPUT]
    Field(0)
      Physical(Power Device.Battery)
      Application(Power Device.UPS)
      Usage(4)
        Battery System.Charging
        Battery System.Discharging
        Battery System.0046
        Battery System.0047
      Logical Minimum(0)
      Logical Maximum(1)
      Report Size(1)
      Report Count(4)
      Report Offset(0)
      Flags( Variable Absolute Volatile )
  FEATURE(1)[FEATURE]
    Field(0)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iManufacturer
      Report Size(8)
      Report Count(1)
      Report Offset(0)
      Flags( Constant Variable Absolute )
    Field(1)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iProduct
      Report Size(8)
      Report Count(1)
      Report Offset(8)
      Flags( Constant Variable Absolute )
    Field(2)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iSerialNumber
      Report Size(8)
      Report Count(1)
      Report Offset(16)
      Flags( Constant Variable Absolute )

Battery System.Charging ---> Sync.Report
Battery System.Discharging ---> Sync.Report
Battery System.0046 ---> Sync.Report
Battery System.0047 ---> Sync.Report

Parsed:

USAGE_PAGE (Power Device)
USAGE (UPS)
COLLECTION (Application)
  USAGE (Battery System)
  COLLECTION (Physical)
    REPORT_ID (1)
    REPORT_SIZE (8)
    REPORT_COUNT (1)

    USAGE (iManufacturer)
    STRING_INDEX (1)
    FEATURE (Constant Variable Absolute)

    USAGE (iProduct)
    STRING_INDEX (2)
    FEATURE (Constant Variable Absolute)

    USAGE (iSerialNumber)
    STRING_INDEX (3)
    FEATURE (Constant Variable Absolute)

    USAGE (Battery)
    COLLECTION (Physical)
      REPORT_ID (2)
      USAGE_PAGE (Battery System)
      REPORT_SIZE (1)
      REPORT_COUNT (4)
      LOGICAL_MINIMUM (0)
      LOGICAL_MAXIMUM (1)
      USAGE (Charging)
      USAGE (Discharging)
      USAGE (Fully Charged)
      USAGE (Fully Discharged)
      INPUT (Variable Absolute Volatile)

      REPORT_COUNT (1)
      REPORT_SIZE (4)
      INPUT (Constant Array)
    END_COLLECTION
  END_COLLECTION
END_COLLECTION

It looks like Linux doesn't recognize the fully charged and fully discharged flags. I also tried removing them and only keeping the charging flag, as well as adding other datapoints, such as absolute state of charge.

In my main.cpp file, I'm sending reports like this:

#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "bsp/board.h"
#include "tusb.h"

int main() {
  board_init();
  tusb_init();

  uint32_t last_run = 0;
  uint8_t report1[] = {1, 2, 3};
  uint8_t report2[] = {0b00000001};
  while (1) {
    tud_task();

    // Send data every 50 ms
    if (board_millis() - last_run > 50 && tud_hid_ready() && !tud_suspended()) {
      last_run = board_millis();
      // tud_hid_report(1, report1, sizeof(report1)); // for some reason if I send 2 reports at once, only #1 is received on the host
      tud_hid_report(2, report2, sizeof(report2));
    }
  }
}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
  switch (report_id) {
    case 1:
      buffer[0] = 1;
      buffer[1] = 2;
      buffer[2] = 3;
      return 3;
    case 2:
      buffer[0] = 0b00000001; // Report charging state
      return 1;
    default:
      return 0;
  }
}

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {
  // echo back anything we received from host
  tud_hid_report(0, buffer, bufsize);
}

I'm not completely sure which is the correct way of sending reports: using tud_hid_get_report_cb or using a loop and tud_hid_report. Any tips regarding this will be appreciated.

The reports on the receiving end look correct to me:

root@user-HP:~# usbhid-dump -a 1:119 -e all
001:119:000:DESCRIPTOR         1680420804.631776
 05 84 09 10 A1 01 85 01 75 08 95 01 09 FD 79 01
 B1 03 09 FE 79 02 B1 03 09 FF 79 03 B1 03 09 12
 A1 00 85 02 05 85 75 01 95 04 17 00 00 00 00 27
 01 00 00 00 09 44 09 45 09 46 09 47 81 82 95 01
 75 04 81 01 C0 C0

Starting dumping interrupt transfer stream
with 1 minute timeout.

001:119:000:STREAM             1680420804.643196
 02 01

001:119:000:STREAM             1680420804.694348
 02 01

001:119:000:STREAM             1680420804.745391
 02 01

001:119:000:STREAM             1680420804.796426
 02 01

001:119:000:STREAM             1680420804.847433
 02 01

001:119:000:STREAM             1680420804.898256
 02 01

Edit 5 Apr 2023: I have done some debugging with Wireshark, as well as some learning about USB endpoints, I made some changes to the file below: changed TUD_HID_DESCRIPTOR to TUD_HID_INOUT_DESCRIPTOR and the endpoint number to 1. Now, transfers from device to host complete with success status rather than No such file or directory. However, there is one problem: the host sends reports to the device using the 0x81 IN endpoint rather than the correct 0x01 OUT endpoint.

Finally, here is my usb_descriptors.cpp:

#include "tusb.h"
#include "report-descriptor/descriptor.hpp"

//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
tusb_desc_device_t const desc_device = {
  .bLength = sizeof(tusb_desc_device_t),
  .bDescriptorType = TUSB_DESC_DEVICE,
  .bcdUSB = 0x0200,
  .bDeviceClass = 0x00,
  .bDeviceSubClass = 0x00,
  .bDeviceProtocol = 0x00,
  .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,

  // Using APC vendor ID, because a UPS from an unknown vendor won't be detected
  .idVendor = 0x051D,
  .idProduct = 0x0003,
  .bcdDevice = 0x0100,

  .iManufacturer = 0x01,
  .iProduct = 0x02,
  .iSerialNumber = 0x03,

  .bNumConfigurations = 0x01
};

// Invoked when received GET DEVICE DESCRIPTOR
// Application return pointer to descriptor
uint8_t const *tud_descriptor_device_cb(void) {
  return (uint8_t const *)&desc_device;
}

//--------------------------------------------------------------------+
// HID Report Descriptor
//--------------------------------------------------------------------+

// Invoked when received GET HID REPORT DESCRIPTOR
// Application return pointer to descriptor
// Descriptor contents must exist long enough for transfer to complete
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
  return hid_report_descriptor_test.data();
}

//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+

enum {ITF_NUM_HID, ITF_NUM_TOTAL};

#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
#define EPNUM_HID 0x01

uint8_t const desc_configuration[] = {
  // Config number, interface count, string index, total length, attribute, power in mA
  TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

  // Interface number, string index, protocol, report descriptor len, EP In & Out address, size & polling interval
  TUD_HID_INOUT_DESCRIPTOR(
    ITF_NUM_HID,
    0,
    HID_ITF_PROTOCOL_NONE,
    hid_report_descriptor_test.size(),
    EPNUM_HID,
    0x80 | EPNUM_HID,
    CFG_TUD_HID_BUFSIZE,
    1
  )
};

// Invoked when received GET CONFIGURATION DESCRIPTOR
// Application return pointer to descriptor
// Descriptor contents must exist long enough for transfer to complete
uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
{
  (void)index; // for multiple configurations
  return desc_configuration;
}
//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+

// array of pointer to string descriptors
char const *string_desc_arr[] = {
  (const char[]){0x09, 0x04}, // 0: is supported language is English (0x0409)
  "American Power Conversion", // 1: Manufacturer
  "UPS", // 2: Product
  "123456", // 3: Serials, should use chip ID
  "LiFePO4", // 4: Battery chemistry - unused for now
};

static uint16_t _desc_str[32];

// Invoked when received GET STRING DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
  (void)langid;

  uint8_t chr_count;

  if (index == 0)
  {
    memcpy(&_desc_str[1], string_desc_arr[0], 2);
    chr_count = 1;
  }
  else
  {
    // Convert ASCII string into UTF-16

    if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])))
      return NULL;

    const char *str = string_desc_arr[index];

    // Cap at max char
    chr_count = strlen(str);
    if (chr_count > 31)
      chr_count = 31;

    for (uint8_t i = 0; i < chr_count; i++)
    {
      _desc_str[1 + i] = str[i];
    }
  }

  // first byte is length (including header), second byte is string type
  _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2);

  return _desc_str;
}

To reiterate, I'm trying to signal a charging state to the host, but the host reports the battery as empty (UPower) and not charging (Plasma Desktop battery indicator), as if there were no reports sent from the device. How do I make UPower react to the reports?

Anton
  • 440
  • 3
  • 10
  • 1
    If you don't get some good advice after 48 hrs, ping me and I'll put up some bounty points. Good luck. – shellter Apr 02 '23 at 15:46
  • 1
    I don't know if it matters, but you are identifying your Application Collection with a usage that is meant for a Physical Collection - 0x00840010 Battery System (Physical Collection). Try changing 0910 to 0904 - usage 0x00840004 UPS (Application Collection) - and see if that makes a difference. – aja Apr 03 '23 at 03:21
  • 1
    @aja Just tried it, no luck, unfortunately. If I change the usage to UPS and leave the application collection, UPower reports the same thing. If I leave the usage set to Battery System and change the collection to Physical, UPower doesn't enumerate the device... Which means that either I missed something in the HID spec and physical / logical collection can't be in the root, or that UPower follows some strange rules when parsing the descriptor. – Anton Apr 03 '23 at 20:10
  • @shellter Thank you so much for the offer. I think I might have to ask you for the bounty. – Anton Apr 03 '23 at 20:26
  • we're not quite at the 48 hr mark. WIll check later today. Have you been able to confirm the hardware is working (that this is not a problem w defective hdwr?) . Does the hdwr vender/mgfr offer any support? Good luck. – shellter Apr 03 '23 at 20:55
  • @shellter Regarding the hardware, the microcontroller is new, so shouldn't be damaged. It still could be defective - I will try to figure out a way to check it, although it seems unlikely because the USB device is recognized and it is possible to read raw data from it. I might also try to seek help from TinyUSB or UPower developers. As for the host PC, it is not likely to be a problem either, but I will test with another one later in the week. – Anton Apr 04 '23 at 10:56
  • Yes, the top-level collection must always be an Application Collection. That gem is only hinted at in the spec by a remark: ""Collection items may be nested, and they are always optional, except for the top-level application collection." – aja Apr 04 '23 at 21:39
  • @aja Thanks for the info. I have played around a little bit more (see the question edits) - it could be an issue at a lower level. Maybe it's in the raw HID communication, but most likely on the USB level. I might consider asking a more general question on Superuser. – Anton Apr 05 '23 at 20:12
  • 1
    @Антон : I think you'll run into a lot of basic user noise on Superuser searching USB. Look at [electronics.se] (3000+ QA searching for `usb`) or [softwareengineering.se] (~100s) . Good luck. – shellter Apr 05 '23 at 23:54
  • After some more research, I figured out that the endpoint behavior is expected. In USB, the host is supposed to poll the device, and a transfer to the IN endpoint is the actual request for data. Other USB devices seem to have similar behavior. I will keep investigating. @shellter Thanks for the advice re sites. – Anton Apr 08 '23 at 10:50
  • I was so focused on your `c` code I forgot you are dealing with Rpi. Found 183 Q/A for `USB HID` on [raspberrypi.se] . Maybe you'll find some hints there. Good luck – shellter Apr 08 '23 at 15:12
  • I had a look, seems like most of those questions are related to Raspberry Pi computers acting as USB hosts. In my case, there is a RPi Pico (which is a microcontroller board, not a single-board computer) acting as a USB device. Thank you. – Anton Apr 08 '23 at 15:24
  • Well don't rule out asking there. It's possible vendors are only watching that board and not really looking at programming questions here on S.E. And generally the rpi community is about challenges, isn't it (-;? Good luck. – shellter Apr 08 '23 at 15:29
  • @plugwash : looking for Rpi people that may have missed this question before. Note some edits by OP to clarify use case. Any useful advice we get (not necessarily a solution) can be worth a 100 points. Tnx for any ideas. – shellter Apr 08 '23 at 17:05
  • @Anton : Did you see [this](https://raspberrypi.stackexchange.com/questions/142506/pi-pico-reading-data-from-uart0-working-while-powered-via-usb-but-receiving-garb) or am I am grasping at straws? Incidentally I am interested in long-term powering of piZero-picos, so hoping you succeed here (-;! – shellter Apr 08 '23 at 17:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253051/discussion-between-anton-and-shellter). – Anton Apr 08 '23 at 19:17

1 Answers1

1

I understand that you have a device and want to send a HID report to the host. Have you tried if the host can send a report to the device? I have a similar setup but not on a Raspberry Pi, but on the STM32G0B1 where I tried to make a port for TinyUSB for the low-level driver. In my case, the host can send a HID report, and it arrives on the device. It is a kind of heartbeat message in my case.

However, the device tries to send a similar message back to the host, but it does not arrive. I traced the data package into the device layer where it is being written into the hardware buffer. The only difference that I found is that it is getting sent out at endpoint number 1, whereas all the descriptors are sent back at endpoint number zero.

So, in my case, there might be an address mismatch accessing the right hardware registers, but I don't know. Maybe I am using the TinyUSB stack in the wrong way. Have you found a solution to your problem on the Raspberry Pi so far?

Shane Warne
  • 1,350
  • 11
  • 24
  • 1
    Thanks for sharing your experience. I have never tried sending reports from host to the device, will let you know if it works out. Regarding the endpoints, 0 is the control endpoint and all others are data endpoints, so this behavior might be normal. But please take this with a grain of salt - you certainly know more than I do about how it works under the hood :) – Anton Apr 13 '23 at 15:41
  • 1
    I have tried running a few examples from the TinyUSB repo. They seem to be working fine - reports can be sent both ways. I also tried comparing their mouse example with my broken mouse test code. The difference is in the report descriptor - when a mouse has at least one button, it works, but when it's just X and Y coordinates, it doesn't. This makes me think that the UPS is missing some usages too. I just asked another, more general question on SO. Thank you. – Anton Apr 15 '23 at 10:44