Files
p 2b9d655656
Alpine 3.23 Success
upscat: fix device filtering
Static allocation is a gift that keeps on giving.

Windows should now work fine in the non-Input polling mode.
2026-07-07 14:25:07 +02:00

1004 lines
28 KiB
C

/*
* upscat.c: USB HID UPS status streaming utility
*
* This program stays independent of the liberty library
* in order to build on Windows.
*
* Copyright (c) 2026, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#include <assert.h>
#include <inttypes.h>
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <wchar.h>
#include <getopt.h>
#include <hidapi.h>
#ifdef _WIN32
#include <sysinfoapi.h> // GetTickCount
#include <synchapi.h> // Sleep
#else
#include <errno.h>
#include <time.h> // clock_gettime
#endif
#include "upscat-usb.h"
#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "upscat"
#ifdef __OpenBSD__
#define hid_init hidapi_hid_init
#endif
#if defined __MINGW_GNU_PRINTF
#define ATTRIBUTE_PRINTF(x, y) __MINGW_GNU_PRINTF((x), (y))
#elif defined __GNUC__
#define ATTRIBUTE_PRINTF(x, y) __attribute__((format(printf, x, y)))
#else
#define ATTRIBUTE_PRINTF(x, y)
#endif
#define countof(array) (sizeof array / sizeof array[0])
static int64_t
get_timestamp_ms(void)
{
#ifdef _WIN32
return GetTickCount64();
#else
struct timespec tp;
if (clock_gettime(CLOCK_MONOTONIC, &tp) < 0) {
fprintf(stderr, "error: clock_gettime: %s", strerror(errno));
exit(EXIT_FAILURE);
}
return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
#endif
}
// --- USB HID report descriptor parser ----------------------------------------
// This parser is simplified for simple readouts.
// hidpar.pdf provides guidelines for better conforming implementations.
enum {
// This is one byte.
// USB HID 6.2.2.7: Report ID zero is reserved and should not be used.
PARSER_REPORT_LIMIT = 256,
// There is no explicit limit on the number of usages.
PARSER_USAGES_LIMIT = 64,
};
enum {
PARSER_ITEM_TYPE_MAIN = 0,
PARSER_ITEM_TYPE_GLOBAL,
PARSER_ITEM_TYPE_LOCAL,
PARSER_ITEM_TYPE_RESERVED,
PARSER_ITEM_TAG_LONG = 0xf,
PARSER_ITEM_TAG_MAIN_INPUT = 0x8,
PARSER_ITEM_TAG_MAIN_OUTPUT = 0x9,
PARSER_ITEM_TAG_MAIN_FEATURE = 0xb,
PARSER_ITEM_TAG_MAIN_COLLECTION = 0xa,
PARSER_ITEM_TAG_MAIN_END_COLLECTION = 0xc,
PARSER_ITEM_TAG_GLOBAL_USAGE_PAGE = 0x0,
PARSER_ITEM_TAG_GLOBAL_LOGICAL_MINIMUM = 0x1,
PARSER_ITEM_TAG_GLOBAL_LOGICAL_MAXIMUM = 0x2,
PARSER_ITEM_TAG_GLOBAL_PHYSICAL_MINIMUM = 0x3,
PARSER_ITEM_TAG_GLOBAL_PHYSICAL_MAXIMUM = 0x4,
PARSER_ITEM_TAG_GLOBAL_UNIT_EXPONENT = 0x5,
PARSER_ITEM_TAG_GLOBAL_UNIT = 0x6,
PARSER_ITEM_TAG_GLOBAL_REPORT_SIZE = 0x7,
PARSER_ITEM_TAG_GLOBAL_REPORT_ID = 0x8,
PARSER_ITEM_TAG_GLOBAL_REPORT_COUNT = 0x9,
PARSER_ITEM_TAG_GLOBAL_PUSH = 0xa,
PARSER_ITEM_TAG_GLOBAL_POP = 0xb,
PARSER_ITEM_TAG_LOCAL_USAGE = 0x0,
PARSER_ITEM_TAG_LOCAL_USAGE_MINIMUM = 0x1,
PARSER_ITEM_TAG_LOCAL_USAGE_MAXIMUM = 0x2,
PARSER_ITEM_TAG_LOCAL_DESIGNATOR_INDEX = 0x3,
PARSER_ITEM_TAG_LOCAL_DESIGNATOR_MINIMUM = 0x4,
PARSER_ITEM_TAG_LOCAL_DESIGNATOR_MAXIMUM = 0x5,
PARSER_ITEM_TAG_LOCAL_STRING_INDEX = 0x7,
PARSER_ITEM_TAG_LOCAL_STRING_MINIMUM = 0x8,
PARSER_ITEM_TAG_LOCAL_STRING_MAXIMUM = 0x9,
PARSER_ITEM_TAG_LOCAL_DELIMITER = 0xa,
};
// Technically, this is for data-type main items, describing fields.
struct parser_field {
// USB HID 6.2.2.9: "Reports can be padded to byte-align fields by [...]
// not declaring a usage for the main item."
// USB HID 6.2.2.8: If there are fewer Usages than controls, the last Usage
// also applies to the remaining controls.
uint32_t usages[PARSER_USAGES_LIMIT];
size_t usages_len;
int32_t logical_minimum;
int32_t logical_maximum;
uint32_t report_size;
uint32_t report_count;
unsigned array__variable : 1;
unsigned absolute__relative : 1;
};
struct parser_report {
uint32_t report_id;
// USB HID 8.4: "reports may not span more than one top-level collection."
uint32_t application_usage;
// There is no explicit limit on the number of fields per report.
struct parser_field fields[16];
size_t fields_len;
};
struct parser {
// There is no explicit limit on the depth of collections.
// USB HID 6.2.2.6: "a Usage item tag must be associated
// with any collection" however many devices simply do not care.
uint32_t collections[16];
size_t collections_len;
struct parser_state_global {
uint32_t usage_page;
int32_t logical_minimum; // \_ If neither is negative,
int32_t logical_maximum; // / the report field is unsigned.
int32_t physical_minimum; // \ These actually
int32_t physical_maximum; // > start as UNDEFINED,
int32_t unit_exponent; // / which we can't express.
int32_t unit;
uint32_t report_size;
uint32_t report_id;
uint32_t report_count;
} global;
struct parser_state_local {
uint32_t usages[PARSER_USAGES_LIMIT];
size_t usages_len;
uint32_t usage_minimum;
uint32_t designator_index;
uint32_t designator_minimum;
uint32_t designator_maximum;
uint32_t string_index;
uint32_t string_minimum;
uint32_t string_maximum;
uint32_t delimiter;
} local;
struct parser_report input[PARSER_REPORT_LIMIT];
struct parser_report feature[PARSER_REPORT_LIMIT];
};
static const char *
parse_item_set(struct parser *parser, uint32_t flags, bool feature)
{
struct parser_state_global *global = &parser->global;
if (!global->report_id)
return "missing Report ID";
if (global->report_id >= PARSER_REPORT_LIMIT)
return "Report ID is too high";
struct parser_report *report = feature
? &parser->feature[global->report_id]
: &parser->input[global->report_id];
if (report->fields_len == countof(report->fields))
return "too many fields per Report";
struct parser_state_local *local = &parser->local;
struct parser_field *field = &report->fields[report->fields_len++];
if (local->usages_len) {
uint32_t last = local->usages[local->usages_len - 1];
// Not bothering to error out on hitting the compile-time limit.
while (local->usages_len < global->report_count
&& local->usages_len < PARSER_USAGES_LIMIT)
local->usages[local->usages_len++] = last;
}
report->report_id = global->report_id;
report->application_usage = parser->collections[0];
memcpy(field->usages, local->usages, sizeof local->usages);
field->usages_len = local->usages_len;
field->logical_minimum = global->logical_minimum;
field->logical_maximum = global->logical_maximum;
field->report_size = global->report_size;
field->report_count = global->report_count;
field->array__variable = (flags >> 1) & 1;
field->absolute__relative = (flags >> 2) & 1;
return NULL;
}
static const char *
parse_item(
struct parser *parser, uint8_t type, uint8_t tag, int32_t s, uint32_t u)
{
switch (type) {
case PARSER_ITEM_TYPE_MAIN: {
const char *err = NULL;
switch (tag) {
break; case PARSER_ITEM_TAG_MAIN_INPUT:
err = parse_item_set(parser, u, false);
break; case PARSER_ITEM_TAG_MAIN_OUTPUT:
// We don't care about these.
break; case PARSER_ITEM_TAG_MAIN_FEATURE:
err = parse_item_set(parser, u, true);
break; case PARSER_ITEM_TAG_MAIN_COLLECTION:
if (parser->local.usages_len > 1)
return "expecting Collections to have at most one usage";
if (!parser->collections_len && u != USB_HID_COLLECTION_APPLICATION)
return "top-level Collections must be Application";
if (parser->collections_len == countof(parser->collections))
return "Collections nested too deep";
parser->collections[parser->collections_len++] =
parser->local.usages_len ? parser->local.usages[0] : 0;
break; case PARSER_ITEM_TAG_MAIN_END_COLLECTION:
if (!parser->collections_len)
return "no Collection to end";
parser->collections_len--;
break; default:
return "unsupported Main item tag";
}
parser->local = (struct parser_state_local) {};
return err;
}
case PARSER_ITEM_TYPE_GLOBAL:
switch (tag) {
break; case PARSER_ITEM_TAG_GLOBAL_USAGE_PAGE:
parser->global.usage_page = u;
break; case PARSER_ITEM_TAG_GLOBAL_LOGICAL_MINIMUM:
parser->global.logical_minimum = s;
break; case PARSER_ITEM_TAG_GLOBAL_LOGICAL_MAXIMUM:
parser->global.logical_maximum = s;
break; case PARSER_ITEM_TAG_GLOBAL_PHYSICAL_MINIMUM:
parser->global.physical_minimum = s;
break; case PARSER_ITEM_TAG_GLOBAL_PHYSICAL_MAXIMUM:
parser->global.physical_maximum = s;
break; case PARSER_ITEM_TAG_GLOBAL_UNIT_EXPONENT:
parser->global.unit_exponent = s;
break; case PARSER_ITEM_TAG_GLOBAL_UNIT:
parser->global.unit = s;
break; case PARSER_ITEM_TAG_GLOBAL_REPORT_SIZE:
if (u > 32)
return "Report Size too large";
parser->global.report_size = u;
break; case PARSER_ITEM_TAG_GLOBAL_REPORT_ID:
parser->global.report_id = u;
break; case PARSER_ITEM_TAG_GLOBAL_REPORT_COUNT:
parser->global.report_count = u;
break; case PARSER_ITEM_TAG_GLOBAL_PUSH:
return "state pushing is not supported";
break; case PARSER_ITEM_TAG_GLOBAL_POP:
return "state pushing is not supported";
break; default:
return "unsupported Global item tag";
}
break;
case PARSER_ITEM_TYPE_LOCAL:
switch (tag) {
break; case PARSER_ITEM_TAG_LOCAL_USAGE:
if (parser->local.usages_len == countof(parser->local.usages))
return "too many Usages per field";
// USB HUT 3.1: Usage ID 0 should always be reserved.
if (!u)
return "zero Usage";
// We should really decide by the data length instead.
if (u < 0x10000)
u = parser->global.usage_page << 16 | u;
parser->local.usages[parser->local.usages_len++] = u;
break; case PARSER_ITEM_TAG_LOCAL_USAGE_MINIMUM:
parser->local.usage_minimum = u;
break; case PARSER_ITEM_TAG_LOCAL_USAGE_MAXIMUM:
// This adds to usages from parser->local.usage_minimum through u.
return "usage ranges are not supported";
break; case PARSER_ITEM_TAG_LOCAL_DESIGNATOR_INDEX:
parser->local.designator_index = u;
break; case PARSER_ITEM_TAG_LOCAL_DESIGNATOR_MINIMUM:
parser->local.designator_minimum = u;
break; case PARSER_ITEM_TAG_LOCAL_DESIGNATOR_MAXIMUM:
parser->local.designator_maximum = u;
break; case PARSER_ITEM_TAG_LOCAL_STRING_INDEX:
parser->local.string_index = u;
break; case PARSER_ITEM_TAG_LOCAL_STRING_MINIMUM:
parser->local.string_minimum = u;
break; case PARSER_ITEM_TAG_LOCAL_STRING_MAXIMUM:
parser->local.string_maximum = u;
break; case PARSER_ITEM_TAG_LOCAL_DELIMITER:
parser->local.delimiter = u;
return "delimiters are not supported";
break; default:
return "unsupported Local item tag";
}
break;
case PARSER_ITEM_TYPE_RESERVED:
// Completely unnecessary.
return "long/reserved items are not supported";
}
return NULL;
}
static const char *
parse_descriptor(struct parser *parser, const uint8_t *descriptor, size_t len)
{
// USB HID 5.2 Report Descriptors
const uint8_t *p = descriptor, *end = p + len;
while (p != end) {
// USB HID 5.3 Generic Item Format
// USB HID 6.2.2.1 Items Types and Tags
uint8_t prefix = *p++,
size = prefix & 0x3,
type = (prefix >> 2) & 0x3,
tag = prefix >> 4;
size += size == 3;
if (p + size > end)
return "item overflow";
uint32_t uvalue = 0;
int32_t svalue = 0;
switch (size) {
break; case 0:
break; case 1:
uvalue = p[0];
svalue = (int8_t) p[0];
break; case 2:
uvalue = p[0] | p[1] << 8;
svalue = (int16_t) (p[0] | p[1] << 8);
break; case 4:
uvalue = p[0] | p[1] << 8 | p[2] << 16 | p[3] << 24;
svalue = (int32_t) uvalue;
}
p += size;
const char *err = parse_item(parser, type, tag, svalue, uvalue);
if (err)
return err;
}
return NULL;
}
// --- Report reader -----------------------------------------------------------
struct report_parser {
const uint8_t *data; ///< Start of data from the report
size_t len; ///< Length of report data
const struct parser_report *report; ///< Report definition
unsigned field; ///< Current field iterator
unsigned subfield; ///< report_count iterator
unsigned bit_position; ///< Current bit position within data
};
static struct report_parser
report_parser_make(
const struct parser_report *report, const uint8_t *data, size_t len)
{
return (struct report_parser) {.data = data, .len = len, .report = report};
}
static int64_t
report_parser_extract(struct report_parser *rp, unsigned nbits, bool is_signed)
{
// XXX: This algorithm seems unnecessarily random-access.
// USB HID 5.8: the little-endian aspect actually needs no special handling.
unsigned byte_offset = rp->bit_position / 8,
shift = rp->bit_position % 8,
nbytes = (shift + nbits + 7) / 8;
uint64_t raw = 0;
for (unsigned i = 0; i < nbytes; i++) {
uint8_t byte = 0;
if (byte_offset + i < rp->len)
byte = rp->data[byte_offset + i];
raw |= (uint64_t) byte << (8 * i);
}
rp->bit_position += nbits;
raw >>= shift;
if (nbits < 64)
raw &= (1ULL << nbits) - 1;
// USB HID 5.8: fields are signed (2's complement)
// unless both Logical Minimum and Logical Maximum are non-negative.
// Do sign extension.
if (is_signed && nbits > 0 && nbits < 64 && (raw & 1ULL << (nbits - 1)))
raw |= ~((1ULL << nbits) - 1);
return (int64_t) raw;
}
static bool
report_parser_parse(struct report_parser *rp, uint32_t *usage, int64_t *value)
{
while (rp->field < rp->report->fields_len) {
const struct parser_field *f = &rp->report->fields[rp->field];
if (!f->report_count) {
rp->field++;
continue;
}
uint32_t u = 0;
if (rp->subfield < f->usages_len)
u = f->usages[rp->subfield];
int64_t v = report_parser_extract(
rp, f->report_size, f->logical_minimum < 0);
if (++rp->subfield == f->report_count) {
rp->field++;
rp->subfield = 0;
}
// Cannot make use of these.
if (f->array__variable == 0 || f->absolute__relative == 1)
continue;
*usage = u;
*value = v;
return true;
}
return false;
}
// --- UPS abstraction ---------------------------------------------------------
struct ups_usage_output {
uint32_t usage; ///< Extended usage, including page
int64_t value; ///< Value
};
static int
ups_usage_output_cmp(const void *a, const void *b)
{
const struct ups_usage_output *aa = (const struct ups_usage_output *) a;
const struct ups_usage_output *bb = (const struct ups_usage_output *) b;
return (aa->usage > bb->usage) - (aa->usage < bb->usage);
}
struct ups {
hid_device *dev; ///< HID device handle
const struct hid_device_info *info; ///< HID device information
struct parser parser; ///< Parsed USB HID report descriptor
bool useful_reports[PARSER_REPORT_LIMIT];
struct ups_usage_output fields[256];
size_t fields_len;
// As a theme, we spend memory in order to limit code and dependencies.
char error[1024];
};
static int
ups_printid(const struct ups *u, char *buffer, size_t len)
{
if (!*u->info->manufacturer_string)
return snprintf(buffer, len, "%s(%04x:%04x): ",
u->info->path, u->info->vendor_id, u->info->product_id);
else if (!wcsncmp(u->info->manufacturer_string, u->info->product_string,
wcslen(u->info->manufacturer_string)))
return snprintf(buffer, len, "%s(%ls): ",
u->info->path, u->info->product_string);
else
return snprintf(buffer, len, "%s(%ls %ls): ", u->info->path,
u->info->manufacturer_string, u->info->product_string);
}
static bool
ups_failf(struct ups *u, const char *format, ...)
ATTRIBUTE_PRINTF(2, 3);
static bool
ups_failf(struct ups *u, const char *format, ...)
{
int len = ups_printid(u, u->error, sizeof u->error);
if (len >= 0) {
va_list ap;
va_start(ap, format);
(void) vsnprintf(u->error + len, sizeof u->error - len, format, ap);
va_end(ap);
}
return false;
}
static unsigned
ups_find_feature_report_with_usage(struct ups *u, uint32_t usage)
{
for (size_t i = 0; i < PARSER_REPORT_LIMIT; i++) {
struct parser_report *r = &u->parser.feature[i];
if (!r->report_id || r->application_usage != USB_HID_USAGE__POWER__UPS)
continue;
for (size_t k = 0; k < r->fields_len; k++) {
struct parser_field *f = &r->fields[k];
// Cannot make use of these.
if (f->array__variable == 0 || f->absolute__relative == 1)
continue;
for (size_t u = 0; u < f->usages_len; u++)
if (f->usages[u] == usage)
return r->report_id;
}
}
return 0;
}
static const uint32_t ups_required_usages[] = {
USB_HID_USAGE__BATTERY_SYSTEM__AC_PRESENT,
USB_HID_USAGE__BATTERY_SYSTEM__REMAINING_CAPACITY,
USB_HID_USAGE__BATTERY_SYSTEM__RUN_TIME_TO_EMPTY,
USB_HID_USAGE__POWER__PERCENT_LOAD,
0
};
static bool
ups_is_required_usage(uint32_t usage)
{
for (size_t i = 0; ups_required_usages[i]; i++)
if (ups_required_usages[i] == usage)
return true;
return false;
}
static bool
ups_is_compatible(struct ups *u)
{
for (size_t i = 0; ups_required_usages[i]; i++) {
unsigned report_id =
ups_find_feature_report_with_usage(u, ups_required_usages[i]);
if (!report_id)
return ups_failf(u, "usage not found: %s",
usb_hid_usage_to_string_full(ups_required_usages[i]));
u->useful_reports[report_id] = true;
}
return true;
}
static bool
ups_open(struct ups *u, const struct hid_device_info *info)
{
memset(u, 0, sizeof *u);
#if 0
// On some systems, we get one Application collection per logical device.
// However, on systems where this is not the case, if there are
// more such collections, these fields only cover the first one of them.
// We'll use the parsed descriptor to figure out compatibility.
uint32_t usage = info->usage_page << 16 | info->usage;
if (usage && usage != USB_HID_USAGE__POWER__UPS)
return ups_failf(u, "unexpected HID usage");
#endif
u->info = info;
hid_device *dev = hid_open_path(info->path);
if (!dev) {
ups_failf(u, "%ls", hid_error(NULL));
goto out1;
}
u->dev = dev;
u->info = hid_get_device_info(dev);
if (!u->info) {
ups_failf(u, "%ls", hid_error(NULL));
goto out2;
}
uint8_t descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE] = {};
int len = hid_get_report_descriptor(dev, descriptor, sizeof descriptor);
if (len < 0) {
ups_failf(u, "failed to read report descriptor");
goto out2;
}
#if DUMP_DESCRIPTORS
for (size_t i = 0; i < (size_t) len; i++)
printf("%02x ", descriptor[i]);
printf("\n");
#endif
const char *err = parse_descriptor(&u->parser, descriptor, len);
if (err) {
ups_failf(u, "failed to parse report descriptor: %s", err);
goto out2;
}
if (ups_is_compatible(u))
return true;
out2:
hid_close(dev);
out1:
u->dev = NULL;
u->info = NULL;
return false;
}
static void
ups_close(struct ups *u)
{
if (u->dev)
hid_close(u->dev);
*u = (struct ups) {};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static size_t
ups_report_len(const struct parser_report *r)
{
size_t total_bits = 0;
for (size_t i = 0; i < r->fields_len; i++)
total_bits += r->fields[i].report_size * r->fields[i].report_count;
return (total_bits + 7) / 8;
}
static bool
ups_parse_report_field(
struct ups *u, const struct ups_usage_output field, bool filter)
{
// Update values of the same usage.
//
// This is not ideal, because, e.g., Voltage can figure in multiple
// Power page physical collections: Power Summary, Input, Output.
//
// It also causes spurious change reports.
for (size_t i = 0; i < u->fields_len; i++) {
if (u->fields[i].usage == field.usage) {
bool changed = u->fields[i].value != field.value;
u->fields[i].value = field.value;
return changed;
}
}
if (filter && !ups_is_required_usage(field.usage))
return false;
if (u->fields_len == countof(u->fields)) {
fprintf(stderr, "warning: too many fields\n");
return false;
}
u->fields[u->fields_len++] = field;
return true;
}
static bool
ups_parse_report(struct ups *u, const struct parser_report *r,
const uint8_t *data, size_t len, bool filter)
{
struct report_parser rp = report_parser_make(r, data, len);
struct ups_usage_output field = {};
bool changed = false;
while (report_parser_parse(&rp, &field.usage, &field.value))
if (ups_parse_report_field(u, field, filter))
changed = true;
qsort(u->fields, u->fields_len, sizeof u->fields[0], ups_usage_output_cmp);
return changed;
}
static void
ups_dump(const struct ups *u)
{
printf("%s,%ls,%ls,%ls", u->info->path, u->info->manufacturer_string,
u->info->product_string, u->info->serial_number);
for (size_t i = 0; i < u->fields_len; i++)
if (u->fields[i].usage)
printf(",%" PRId64, u->fields[i].value);
putchar('\n');
fflush(stdout);
}
/// Retrieve all reports which contain the data we're interested in.
static bool
ups_rescan(struct ups *u, int verbose, bool *changed)
{
for (unsigned i = 0; i < countof(u->useful_reports); i++) {
if (verbose < 2 && !u->useful_reports[i])
continue;
const struct parser_report *r = &u->parser.feature[i];
if (!r->report_id)
continue;
size_t len = 1 + ups_report_len(r);
uint8_t buf[1024] = {i};
if (hid_get_feature_report(u->dev, buf, len) < 0) {
ups_failf(u, "Get_Feature failed: %ls", hid_error(u->dev));
return false;
}
if (ups_parse_report(u, r, buf + 1, len - 1, !verbose))
*changed = true;
}
return true;
}
static bool
ups_watch(struct ups *u, int interval, int verbose)
{
int until_rescan = interval;
uint8_t buf[1024] = {};
int res = 0;
while (true) {
#ifdef _WIN32
// FIXME: Windows seems to fail reads with "Incorrect function".
// For now, at least try to make do with simple rescans.
Sleep(until_rescan);
#else
int64_t stamp = get_timestamp_ms();
if ((res = hid_read_timeout(
u->dev, buf, sizeof buf, until_rescan)) < 0)
return ups_failf(u, "watch: %ls", hid_read_error(u->dev));
if (interval >= 0) {
if ((until_rescan -= get_timestamp_ms() - stamp) < 0)
until_rescan = 0;
}
#endif
if (!res) {
// Non-negative intervals allow a timeout:
// do a full rescan, and prime the next interval.
until_rescan = interval;
bool changed = false;
if (!ups_rescan(u, verbose, &changed))
return false;
if (changed)
ups_dump(u);
} else {
const struct parser_report *r = &u->parser.input[buf[0]];
if (r->report_id && ups_parse_report(u, r, buf + 1, res - 1, true))
ups_dump(u);
}
}
return true;
}
// --- Tests -------------------------------------------------------------------
#ifdef TESTING
static struct ups test_ups = {.info = &(const struct hid_device_info) {
.path = "",
.manufacturer_string = L"Test",
.product_string = L"Test",
}};
static bool
test_parse_descriptor_file(const char *path)
{
FILE *fp = fopen(path, "rb");
if (!fp) {
perror(path);
return false;
}
uint8_t data[65535] = {};
size_t len = fread(data, 1, sizeof data, fp);
if (ferror(fp)) {
perror(path);
fclose(fp);
return false;
}
fclose(fp);
struct ups *u = &test_ups;
memset(&u->parser, 0, sizeof u->parser);
const char *err = parse_descriptor(&u->parser, data, len);
if (err) {
fprintf(stderr, "%s: failed to parse report descriptor: %s\n",
path, err);
return false;
}
if (!ups_is_compatible(u)) {
fprintf(stderr, "%s: incompatible: %s\n", path, u->error);
return false;
}
return true;
}
int
main(int argc, char *argv[])
{
// The most complex part of this program is the report descriptor parser,
// so that's what we'll test. All passed files are expected to be valid
// and useful descriptors of a UPS-class device.
int status = 0;
for (int i = 1; i < argc; i++)
if (!test_parse_descriptor_file(argv[i]))
status = 1;
return status;
}
#define main main_shadowed
#endif // TESTING
// --- Main --------------------------------------------------------------------
static const char *
run(struct ups *devices, size_t len, bool watch, int interval, int verbose)
{
if (!len)
return "no devices found";
// The CSV format is inconvenient, as verbose output would be heterogenous.
// We could realistically switch to JSON, as we only need to produce it,
// not parse it, and that isn't too hard.
if (len > 1 && verbose) {
fprintf(stderr,
"warning: multiple devices, disabling verbose output\n");
verbose = 0;
}
for (size_t i = 0; i < len; i++) {
struct ups *u = &devices[i];
bool changed = false;
if (!ups_rescan(u, verbose, &changed))
return u->error;
// In non-verbose mode, the order must be made the same for all fields.
if (u == devices) {
printf("path,manufacturer,product,serial");
for (size_t i = 0; i < u->fields_len; i++)
if (u->fields[i].usage)
printf(",%s", usb_hid_usage_to_string(u->fields[i].usage));
putchar('\n');
}
ups_dump(u);
}
if (watch) {
// TODO(p): We could support multiple devices, but it's tricky reading
// from all of them at once. Options:
// - Run hid_read_timeout() for all of them in a sequence.
// But any lags are in principle undesired.
// - Run this from multiple threads. This is a bit more code.
if (len > 1)
fprintf(stderr, "warning: multiple devices, watching the first\n");
struct ups *u = devices;
if (!ups_watch(u, interval, verbose))
return u->error;
}
return NULL;
}
static const char *usage = "Usage: %s OPTION...\n\n"
" -l, --list List all recognised UPS devices.\n"
" -n, --nowatch Do not watch for changes.\n"
" -i, --interval Polling interval in milliseconds, negative to disable.\n"
" -v, --verbose Output a bit more information.\n"
" -h, --help Display this help and exit.\n"
" -V, --version Output version information and exit.\n";
int
main(int argc, char *argv[])
{
const char *name = argv[0];
static struct option opts[] = {
{"list", no_argument, NULL, 'l'},
{"nowatch", no_argument, NULL, 'n'},
{"interval", required_argument, NULL, 'i'},
{"help", no_argument, NULL, 'h'},
{"verbose", no_argument, NULL, 'v'},
{"version", no_argument, NULL, 'V'},
{}
};
// Some devices' Input reports simply never happen.
// Eaton UPS Companion scans everything in 3-second intervals.
//
// On the other hand, CyberPower's continuous reporting makes our rescans
// let the interrupt queue fill up, after which we may pick up slightly
// older data from there. It creates an opportunity for flip-flopping.
long interval = 5000;
bool list = false, watch = true;
int verbose = 0;
int c = 0;
while ((c = getopt_long(argc, argv, "lni:vhV", opts, NULL)) != -1)
switch (c) {
case 'l':
list = true;
break;
case 'n':
watch = false;
break;
case 'i':
if ((interval = strtol(optarg, NULL, 10)) < 0)
interval = -1;
else if (interval > INT_MAX)
interval = INT_MAX;
break;
case 'v':
verbose++;
break;
case 'h':
printf(usage, name);
return 0;
case 'V':
printf(PROGRAM_NAME " " PROGRAM_VERSION "\n");
return 0;
default:
fprintf(stderr, "Unknown option\n");
fprintf(stderr, usage, name);
return 1;
}
argc -= optind;
argv += optind;
if (argc != 0) {
fprintf(stderr, usage, name);
return 1;
}
// This is safe to call repeatedly, it just might reset the locale.
// It's actually not needed, so we might even leave it out.
if (hid_init()) {
fprintf(stderr, "error: %ls\n", hid_error(NULL));
return 1;
}
// Not many of these structures would fit on the stack.
static struct ups devices[16] = {};
size_t devices_len = 0;
struct hid_device_info *devs = hid_enumerate(0, 0), *p = devs;
for (; p; p = p->next) {
if (devices_len == countof(devices)) {
fprintf(stderr, "warning: too many devices, skipping the rest");
break;
}
struct ups *u = &devices[devices_len];
if (ups_open(u, p))
devices_len++;
else if (verbose)
fprintf(stderr, "error: %s\n", u->error);
}
hid_free_enumeration(devs);
const char *err = NULL;
if (list) {
for (size_t i = 0; i < devices_len; i++) {
struct ups *u = &devices[i];
printf("path,manufacturer,product,serial\n%s,%ls,%ls,%ls\n",
u->info->path, u->info->manufacturer_string,
u->info->product_string, u->info->serial_number);
}
} else if ((err = run(devices, devices_len, watch, interval, verbose))) {
fprintf(stderr, "error: %s\n", err);
}
for (size_t i = 0; i < devices_len; i++)
ups_close(&devices[i]);
return err != NULL;
}