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?