Add upscat
Alpine 3.23 Scripts failed

This is a simple utility to track UPS status.
This commit is contained in:
2026-07-04 10:45:18 +02:00
parent 97b86c168e
commit 2c30ddfa3d
9 changed files with 1813 additions and 1 deletions
+15
View File
@@ -35,6 +35,7 @@ endif ()
# Dependencies
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (IconUtils)
include (CTest)
find_package (PkgConfig REQUIRED)
pkg_check_modules (libusb libusb-1.0)
@@ -95,6 +96,20 @@ if (WITH_HIDAPI)
target_include_directories (eizoctl PUBLIC ${hidapi_INCLUDE_DIRS})
target_link_directories (eizoctl PUBLIC ${hidapi_LIBRARY_DIRS})
target_link_libraries (eizoctl ${hidapi_LIBRARIES})
list (APPEND targets upscat)
add_executable (upscat upscat.c)
target_include_directories (upscat PUBLIC ${hidapi_INCLUDE_DIRS})
target_link_directories (upscat PUBLIC ${hidapi_LIBRARY_DIRS})
target_link_libraries (upscat ${hidapi_LIBRARIES})
endif ()
if (WITH_HIDAPI AND BUILD_TESTING)
add_executable (test-upscat $<TARGET_PROPERTY:upscat,SOURCES>)
set_target_properties (test-upscat PROPERTIES COMPILE_DEFINITIONS TESTING)
target_link_libraries (test-upscat $<TARGET_PROPERTY:upscat,LINK_LIBRARIES>)
add_test (NAME test-upscat
COMMAND test-upscat upscat-eaton3p.bin upscat-cyberpower.bin
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}")
endif ()
if (WITH_HIDAPI AND WIN32)
list (APPEND targets_gui eizoctltray)
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2013, 2024 - 2025, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2013, 2024 - 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.
+5
View File
@@ -1,3 +1,8 @@
Unreleased
* Added upscat for streaming status from UPS devices
1.2.0 (2026-01-04)
* eizoctl: added a --quiet option to suppress information and/or errors
+22
View File
@@ -102,6 +102,28 @@ Make sure to let the Windows Razer Synapse tool upgrade the firmware to the
newest version before running the program. There might be some issues otherwise
due to protocol changes, although I don't really deem it very probable.
upscat
~~~~~~
_upscat_ streams the basic status of UPS devices using the USB HID protocol,
in CSV format. You'll get the power status, remaining capacity, run time,
and load percentage. It's been verified with Eaton 3S/3P series and CyberPower
BR1200ELCD, both working quite differently.
This program is meant to be a simple replacement for NUT:
```sh
upscat | awk -F, '
BEGIN {
getline
for (i = 1; i <= NF; i++) csv[$i] = i
}
!$csv["AC Present"] && $csv["Remaining Capacity"] < 50 {
print "On battery with " $csv["Remaining Capacity"] "% left, suspending"
system("systemctl suspend")
exit
}'
```
Packages
--------
Regular releases are sporadic. git master should be stable enough.
Binary file not shown.
Binary file not shown.
+154
View File
@@ -0,0 +1,154 @@
#!/bin/sh -e
# upscat-gen-usb.sh: convert tables from the USB HID specification to C code
#
# Copyright (c) 2026, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
if [ ! -e hut1_7.pdf ]
then curl -LO https://www.usb.org/sites/default/files/hut1_7.pdf
fi
# Our program doesn't need all of them, so we're filtering them here.
process() (qpdf --show-attachment=HidUsageTables.json hut1_7.pdf \
| jq -r '.UsagePages[] | select(.Name | IN("Battery System", "Power")) |
'"$1"' | @tsv' | sort -n -k 1,1 -k 2,2 | awk -F'\t' '
function ident(s) { gsub(/[^[:alnum:]]/, "_", s); return s } '"{ $2 }")
pages='[.Id, .Name]'
usages='. as $page | $page.UsageIds[] |
[$page.Id, .Id, $page.Name, (.Name | gsub("\\\\_"; "_")),
(.Kinds | join("/"))]'
generated='. as $page | .UsageIdGenerator | select(.) |
[$page.Id, .StartUsageId, .EndUsageId, $page.Name, .NamePrefix,
(.Kinds | join("/"))]'
cat <<EOF >upscat-usb.h
// Code generated by $0. DO NOT EDIT.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
enum {
USB_HID_COLLECTION_PHYSICAL = 0x00,
USB_HID_COLLECTION_APPLICATION = 0x01,
USB_HID_COLLECTION_LOGICAL = 0x02,
USB_HID_COLLECTION_REPORT = 0x03,
USB_HID_COLLECTION_NAMED_ARRAY = 0x04,
USB_HID_COLLECTION_USAGE_SWITCH = 0x05,
USB_HID_COLLECTION_USAGE_MODIFIER = 0x06,
};
enum {
$(process "$pages" '
printf("\t/// %s\n", $2)
printf("\tUSB_HID_USAGE_PAGE__%s = 0x%04x,\n", toupper(ident($2)), $1)
')
};
static const struct usb_hid_usage_page_info {
uint16_t usage_page;
const char *name;
} usb_hid_usage_pages[] = {
$(process "$pages" 'printf("\t{0x%04x, \"%s\"},\n", $1, $2)')
};
$(process "$usages" '
printf("/// %s: %s (%s)\n", $3, $4, $5)
printf("#define USB_HID_USAGE__%s__%s 0x%04x%04x\n",
toupper(ident($3)), toupper(ident($4)), $1, $2)
')
static const struct usb_hid_usage_info {
uint32_t usage;
const char *name;
} usb_hid_usages[] = {
$(process "$usages" 'printf("\t{0x%04x%04x, \"%s\"},\n", $1, $2, $4)')
};
static const struct usb_hid_usage_range_info {
uint32_t usage_start;
uint32_t usage_end;
const char *name_prefix;
} usb_hid_usages_ranged[] = {
$(process "$generated" '
printf("\t// %s (%s)\n", $4, $6)
printf("\t{0x%04x%04x, 0x%04x%04x, \"%s\"},\n", $1, $2, $1, $3, $5)
')
};
static int
usb_hid_usage_pages_cmp(const void *a, const void *b)
{
const uint16_t *key = a;
const struct usb_hid_usage_page_info *entry = b;
return (*key > entry->usage_page) - (*key < entry->usage_page);
}
static int
usb_hid_usages_cmp(const void *a, const void *b)
{
const uint32_t *key = a;
const struct usb_hid_usage_info *entry = b;
return (*key > entry->usage) - (*key < entry->usage);
}
static int
usb_hid_usages_ranged_cmp(const void *a, const void *b)
{
const uint32_t *key = a;
const struct usb_hid_usage_range_info *entry = b;
return (*key > entry->usage_end) - (*key < entry->usage_start);
}
static const char *
usb_hid_usage_page_to_string(uint16_t usage_page)
{
const struct usb_hid_usage_page_info *entry = bsearch(
&usage_page, usb_hid_usage_pages,
sizeof usb_hid_usage_pages / sizeof usb_hid_usage_pages[0],
sizeof usb_hid_usage_pages[0], usb_hid_usage_pages_cmp);
if (entry)
return entry->name;
static char buffer[128];
snprintf(buffer, sizeof buffer, "%04x", usage_page);
return buffer;
}
static const char *
usb_hid_usage_to_string(uint32_t usage)
{
static char buffer[128];
const struct usb_hid_usage_info *info = bsearch(
&usage, usb_hid_usages,
sizeof usb_hid_usages / sizeof usb_hid_usages[0],
sizeof usb_hid_usages[0], usb_hid_usages_cmp);
if (info)
return info->name;
const struct usb_hid_usage_range_info *range = bsearch(
&usage, usb_hid_usages_ranged,
sizeof usb_hid_usages_ranged / sizeof usb_hid_usages_ranged[0],
sizeof usb_hid_usages_ranged[0], usb_hid_usages_ranged_cmp);
if (range) {
snprintf(buffer, sizeof buffer,
"%s %u", range->name_prefix, (unsigned) (usage & 0xffff));
return buffer;
}
snprintf(buffer, sizeof buffer, "%04x", (unsigned) (usage & 0xffff));
return buffer;
}
static const char *
usb_hid_usage_to_string_full(uint32_t usage)
{
static char buffer[128];
snprintf(buffer, sizeof buffer, "%s: %s",
usb_hid_usage_page_to_string(usage >> 16),
usb_hid_usage_to_string(usage));
return buffer;
}
EOF
+624
View File
@@ -0,0 +1,624 @@
// Code generated by ./upscat-gen-usb.sh. DO NOT EDIT.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
enum {
USB_HID_COLLECTION_PHYSICAL = 0x00,
USB_HID_COLLECTION_APPLICATION = 0x01,
USB_HID_COLLECTION_LOGICAL = 0x02,
USB_HID_COLLECTION_REPORT = 0x03,
USB_HID_COLLECTION_NAMED_ARRAY = 0x04,
USB_HID_COLLECTION_USAGE_SWITCH = 0x05,
USB_HID_COLLECTION_USAGE_MODIFIER = 0x06,
};
enum {
/// Power
USB_HID_USAGE_PAGE__POWER = 0x0084,
/// Battery System
USB_HID_USAGE_PAGE__BATTERY_SYSTEM = 0x0085,
};
static const struct usb_hid_usage_page_info {
uint16_t usage_page;
const char *name;
} usb_hid_usage_pages[] = {
{0x0084, "Power"},
{0x0085, "Battery System"},
};
/// Power: iName (SV)
#define USB_HID_USAGE__POWER__INAME 0x00840001
/// Power: Present Status (CL)
#define USB_HID_USAGE__POWER__PRESENT_STATUS 0x00840002
/// Power: Changed Status (CL)
#define USB_HID_USAGE__POWER__CHANGED_STATUS 0x00840003
/// Power: UPS (CA)
#define USB_HID_USAGE__POWER__UPS 0x00840004
/// Power: Power Supply (CA)
#define USB_HID_USAGE__POWER__POWER_SUPPLY 0x00840005
/// Power: Battery System (CP)
#define USB_HID_USAGE__POWER__BATTERY_SYSTEM 0x00840010
/// Power: Battery System Id (SV)
#define USB_HID_USAGE__POWER__BATTERY_SYSTEM_ID 0x00840011
/// Power: Battery (CP)
#define USB_HID_USAGE__POWER__BATTERY 0x00840012
/// Power: Battery Id (SV)
#define USB_HID_USAGE__POWER__BATTERY_ID 0x00840013
/// Power: Charger (CP)
#define USB_HID_USAGE__POWER__CHARGER 0x00840014
/// Power: Charger Id (SV)
#define USB_HID_USAGE__POWER__CHARGER_ID 0x00840015
/// Power: Power Converter (CP)
#define USB_HID_USAGE__POWER__POWER_CONVERTER 0x00840016
/// Power: Power Converter Id (SV)
#define USB_HID_USAGE__POWER__POWER_CONVERTER_ID 0x00840017
/// Power: Outlet System (CP)
#define USB_HID_USAGE__POWER__OUTLET_SYSTEM 0x00840018
/// Power: Outlet System Id (SV)
#define USB_HID_USAGE__POWER__OUTLET_SYSTEM_ID 0x00840019
/// Power: Input (CP)
#define USB_HID_USAGE__POWER__INPUT 0x0084001a
/// Power: Input Id (SV)
#define USB_HID_USAGE__POWER__INPUT_ID 0x0084001b
/// Power: Output (CP)
#define USB_HID_USAGE__POWER__OUTPUT 0x0084001c
/// Power: Output Id (SV)
#define USB_HID_USAGE__POWER__OUTPUT_ID 0x0084001d
/// Power: Flow (CP)
#define USB_HID_USAGE__POWER__FLOW 0x0084001e
/// Power: Flow Id (SV)
#define USB_HID_USAGE__POWER__FLOW_ID 0x0084001f
/// Power: Outlet (CP)
#define USB_HID_USAGE__POWER__OUTLET 0x00840020
/// Power: Outlet Id (SV)
#define USB_HID_USAGE__POWER__OUTLET_ID 0x00840021
/// Power: Gang (CL/CP)
#define USB_HID_USAGE__POWER__GANG 0x00840022
/// Power: Gang Id (SV)
#define USB_HID_USAGE__POWER__GANG_ID 0x00840023
/// Power: Power Summary (CL/CP)
#define USB_HID_USAGE__POWER__POWER_SUMMARY 0x00840024
/// Power: Power Summary Id (SV)
#define USB_HID_USAGE__POWER__POWER_SUMMARY_ID 0x00840025
/// Power: Voltage (DV)
#define USB_HID_USAGE__POWER__VOLTAGE 0x00840030
/// Power: Current (DV)
#define USB_HID_USAGE__POWER__CURRENT 0x00840031
/// Power: Frequency (DV)
#define USB_HID_USAGE__POWER__FREQUENCY 0x00840032
/// Power: Apparent Power (DV)
#define USB_HID_USAGE__POWER__APPARENT_POWER 0x00840033
/// Power: Active Power (DV)
#define USB_HID_USAGE__POWER__ACTIVE_POWER 0x00840034
/// Power: Percent Load (DV)
#define USB_HID_USAGE__POWER__PERCENT_LOAD 0x00840035
/// Power: Temperature (DV)
#define USB_HID_USAGE__POWER__TEMPERATURE 0x00840036
/// Power: Humidity (DV)
#define USB_HID_USAGE__POWER__HUMIDITY 0x00840037
/// Power: Bad Count (DV)
#define USB_HID_USAGE__POWER__BAD_COUNT 0x00840038
/// Power: Config Voltage (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_VOLTAGE 0x00840040
/// Power: Config Current (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_CURRENT 0x00840041
/// Power: Config Frequency (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_FREQUENCY 0x00840042
/// Power: Config Apparent Power (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_APPARENT_POWER 0x00840043
/// Power: Config Active Power (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_ACTIVE_POWER 0x00840044
/// Power: Config Percent Load (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_PERCENT_LOAD 0x00840045
/// Power: Config Temperature (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_TEMPERATURE 0x00840046
/// Power: Config Humidity (SV/DV)
#define USB_HID_USAGE__POWER__CONFIG_HUMIDITY 0x00840047
/// Power: Switch On Control (DV)
#define USB_HID_USAGE__POWER__SWITCH_ON_CONTROL 0x00840050
/// Power: Switch Off Control (DV)
#define USB_HID_USAGE__POWER__SWITCH_OFF_CONTROL 0x00840051
/// Power: Toggle Control (DV)
#define USB_HID_USAGE__POWER__TOGGLE_CONTROL 0x00840052
/// Power: Low Voltage Transfer (DV)
#define USB_HID_USAGE__POWER__LOW_VOLTAGE_TRANSFER 0x00840053
/// Power: High Voltage Transfer (DV)
#define USB_HID_USAGE__POWER__HIGH_VOLTAGE_TRANSFER 0x00840054
/// Power: Delay Before Reboot (DV)
#define USB_HID_USAGE__POWER__DELAY_BEFORE_REBOOT 0x00840055
/// Power: Delay Before Startup (DV)
#define USB_HID_USAGE__POWER__DELAY_BEFORE_STARTUP 0x00840056
/// Power: Delay Before Shutdown (DV)
#define USB_HID_USAGE__POWER__DELAY_BEFORE_SHUTDOWN 0x00840057
/// Power: Test (DV)
#define USB_HID_USAGE__POWER__TEST 0x00840058
/// Power: Module Reset (DV)
#define USB_HID_USAGE__POWER__MODULE_RESET 0x00840059
/// Power: Audible Alarm Control (DV)
#define USB_HID_USAGE__POWER__AUDIBLE_ALARM_CONTROL 0x0084005a
/// Power: Present (DF)
#define USB_HID_USAGE__POWER__PRESENT 0x00840060
/// Power: Good (DF)
#define USB_HID_USAGE__POWER__GOOD 0x00840061
/// Power: Internal Failure (DF)
#define USB_HID_USAGE__POWER__INTERNAL_FAILURE 0x00840062
/// Power: Voltag Out Of Range (DF)
#define USB_HID_USAGE__POWER__VOLTAG_OUT_OF_RANGE 0x00840063
/// Power: Frequency Out Of Range (DF)
#define USB_HID_USAGE__POWER__FREQUENCY_OUT_OF_RANGE 0x00840064
/// Power: Overload (DF)
#define USB_HID_USAGE__POWER__OVERLOAD 0x00840065
/// Power: Over Charged (DF)
#define USB_HID_USAGE__POWER__OVER_CHARGED 0x00840066
/// Power: Over Temperature (DF)
#define USB_HID_USAGE__POWER__OVER_TEMPERATURE 0x00840067
/// Power: Shutdown Requested (DF)
#define USB_HID_USAGE__POWER__SHUTDOWN_REQUESTED 0x00840068
/// Power: Shutdown Imminent (DF)
#define USB_HID_USAGE__POWER__SHUTDOWN_IMMINENT 0x00840069
/// Power: Switch On/Off (DF)
#define USB_HID_USAGE__POWER__SWITCH_ON_OFF 0x0084006b
/// Power: Switchable (DF)
#define USB_HID_USAGE__POWER__SWITCHABLE 0x0084006c
/// Power: Used (DF)
#define USB_HID_USAGE__POWER__USED 0x0084006d
/// Power: Boost (DF)
#define USB_HID_USAGE__POWER__BOOST 0x0084006e
/// Power: Buck (DF)
#define USB_HID_USAGE__POWER__BUCK 0x0084006f
/// Power: Initialized (DF)
#define USB_HID_USAGE__POWER__INITIALIZED 0x00840070
/// Power: Tested (DF)
#define USB_HID_USAGE__POWER__TESTED 0x00840071
/// Power: Awaiting Power (DF)
#define USB_HID_USAGE__POWER__AWAITING_POWER 0x00840072
/// Power: Communication Lost (DF)
#define USB_HID_USAGE__POWER__COMMUNICATION_LOST 0x00840073
/// Power: iManufacturer (SV)
#define USB_HID_USAGE__POWER__IMANUFACTURER 0x008400fd
/// Power: iProduct (SV)
#define USB_HID_USAGE__POWER__IPRODUCT 0x008400fe
/// Power: iSerialNumber (SV)
#define USB_HID_USAGE__POWER__ISERIALNUMBER 0x008400ff
/// Battery System: Smart Battery Battery Mode (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_BATTERY_MODE 0x00850001
/// Battery System: Smart Battery Battery Status (NAry)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_BATTERY_STATUS 0x00850002
/// Battery System: Smart Battery Alarm Warning (NAry)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_ALARM_WARNING 0x00850003
/// Battery System: Smart Battery Charger Mode (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_CHARGER_MODE 0x00850004
/// Battery System: Smart Battery Charger Status (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_CHARGER_STATUS 0x00850005
/// Battery System: Smart Battery Charger Spec Info (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_CHARGER_SPEC_INFO 0x00850006
/// Battery System: Smart Battery Selector State (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_SELECTOR_STATE 0x00850007
/// Battery System: Smart Battery Selector Presets (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_SELECTOR_PRESETS 0x00850008
/// Battery System: Smart Battery Selector Info (CL)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_SELECTOR_INFO 0x00850009
/// Battery System: Optional Mfg Function 1 (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__OPTIONAL_MFG_FUNCTION_1 0x00850010
/// Battery System: Optional Mfg Function 2 (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__OPTIONAL_MFG_FUNCTION_2 0x00850011
/// Battery System: Optional Mfg Function 3 (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__OPTIONAL_MFG_FUNCTION_3 0x00850012
/// Battery System: Optional Mfg Function 4 (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__OPTIONAL_MFG_FUNCTION_4 0x00850013
/// Battery System: Optional Mfg Function 5 (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__OPTIONAL_MFG_FUNCTION_5 0x00850014
/// Battery System: Connection To SM Bus (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__CONNECTION_TO_SM_BUS 0x00850015
/// Battery System: Output Connection (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__OUTPUT_CONNECTION 0x00850016
/// Battery System: Charger Connection (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGER_CONNECTION 0x00850017
/// Battery System: Battery Insertion (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__BATTERY_INSERTION 0x00850018
/// Battery System: Use Next (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__USE_NEXT 0x00850019
/// Battery System: OK To Use (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__OK_TO_USE 0x0085001a
/// Battery System: Battery Supported (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__BATTERY_SUPPORTED 0x0085001b
/// Battery System: Selector Revision (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__SELECTOR_REVISION 0x0085001c
/// Battery System: Charging Indicator (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGING_INDICATOR 0x0085001d
/// Battery System: Manufacturer Access (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__MANUFACTURER_ACCESS 0x00850028
/// Battery System: Remaining Capacity Limit (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__REMAINING_CAPACITY_LIMIT 0x00850029
/// Battery System: Remaining Time Limit (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__REMAINING_TIME_LIMIT 0x0085002a
/// Battery System: At Rate (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AT_RATE 0x0085002b
/// Battery System: Capacity Mode (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CAPACITY_MODE 0x0085002c
/// Battery System: Broadcast To Charger (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__BROADCAST_TO_CHARGER 0x0085002d
/// Battery System: Primary Battery (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__PRIMARY_BATTERY 0x0085002e
/// Battery System: Charge Controller (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGE_CONTROLLER 0x0085002f
/// Battery System: Terminate Charge (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__TERMINATE_CHARGE 0x00850040
/// Battery System: Terminate Discharge (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__TERMINATE_DISCHARGE 0x00850041
/// Battery System: Below Remaining Capacity Limit (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__BELOW_REMAINING_CAPACITY_LIMIT 0x00850042
/// Battery System: Remaining Time Limit Expired (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__REMAINING_TIME_LIMIT_EXPIRED 0x00850043
/// Battery System: Charging (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGING 0x00850044
/// Battery System: Discharging (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__DISCHARGING 0x00850045
/// Battery System: Fully Charged (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__FULLY_CHARGED 0x00850046
/// Battery System: Fully Discharged (Sel)
#define USB_HID_USAGE__BATTERY_SYSTEM__FULLY_DISCHARGED 0x00850047
/// Battery System: Conditioning Flag (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__CONDITIONING_FLAG 0x00850048
/// Battery System: At Rate OK (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__AT_RATE_OK 0x00850049
/// Battery System: Smart Battery Error Code (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__SMART_BATTERY_ERROR_CODE 0x0085004a
/// Battery System: Need Replacement (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__NEED_REPLACEMENT 0x0085004b
/// Battery System: At Rate Time To Full (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AT_RATE_TIME_TO_FULL 0x00850060
/// Battery System: At Rate Time To Empty (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AT_RATE_TIME_TO_EMPTY 0x00850061
/// Battery System: Average Current (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AVERAGE_CURRENT 0x00850062
/// Battery System: Max Error (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__MAX_ERROR 0x00850063
/// Battery System: Relative State Of Charge (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__RELATIVE_STATE_OF_CHARGE 0x00850064
/// Battery System: Absolute State Of Charge (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__ABSOLUTE_STATE_OF_CHARGE 0x00850065
/// Battery System: Remaining Capacity (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__REMAINING_CAPACITY 0x00850066
/// Battery System: Full Charge Capacity (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__FULL_CHARGE_CAPACITY 0x00850067
/// Battery System: Run Time To Empty (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__RUN_TIME_TO_EMPTY 0x00850068
/// Battery System: Average Time To Empty (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AVERAGE_TIME_TO_EMPTY 0x00850069
/// Battery System: Average Time To Full (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AVERAGE_TIME_TO_FULL 0x0085006a
/// Battery System: Cycle Count (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CYCLE_COUNT 0x0085006b
/// Battery System: Battery Pack Model Level (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__BATTERY_PACK_MODEL_LEVEL 0x00850080
/// Battery System: Internal Charge Controller (SF)
#define USB_HID_USAGE__BATTERY_SYSTEM__INTERNAL_CHARGE_CONTROLLER 0x00850081
/// Battery System: Primary Battery Support (SF)
#define USB_HID_USAGE__BATTERY_SYSTEM__PRIMARY_BATTERY_SUPPORT 0x00850082
/// Battery System: Design Capacity (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__DESIGN_CAPACITY 0x00850083
/// Battery System: Specification Info (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__SPECIFICATION_INFO 0x00850084
/// Battery System: Manufacture Date (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__MANUFACTURE_DATE 0x00850085
/// Battery System: Serial Number (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__SERIAL_NUMBER 0x00850086
/// Battery System: iManufacturer Name (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__IMANUFACTURER_NAME 0x00850087
/// Battery System: iDevice Name (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__IDEVICE_NAME 0x00850088
/// Battery System: iDevice Chemistry (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__IDEVICE_CHEMISTRY 0x00850089
/// Battery System: Manufacturer Data (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__MANUFACTURER_DATA 0x0085008a
/// Battery System: Rechargable (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__RECHARGABLE 0x0085008b
/// Battery System: Warning Capacity Limit (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__WARNING_CAPACITY_LIMIT 0x0085008c
/// Battery System: Capacity Granularity 1 (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CAPACITY_GRANULARITY_1 0x0085008d
/// Battery System: Capacity Granularity 2 (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CAPACITY_GRANULARITY_2 0x0085008e
/// Battery System: iOEM Information (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__IOEM_INFORMATION 0x0085008f
/// Battery System: Inhibit Charge (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__INHIBIT_CHARGE 0x008500c0
/// Battery System: Enable Polling (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__ENABLE_POLLING 0x008500c1
/// Battery System: Reset To Zero (DF)
#define USB_HID_USAGE__BATTERY_SYSTEM__RESET_TO_ZERO 0x008500c2
/// Battery System: AC Present (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__AC_PRESENT 0x008500d0
/// Battery System: Battery Present (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__BATTERY_PRESENT 0x008500d1
/// Battery System: Power Fail (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__POWER_FAIL 0x008500d2
/// Battery System: Alarm Inhibited (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__ALARM_INHIBITED 0x008500d3
/// Battery System: Thermistor Under Range (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__THERMISTOR_UNDER_RANGE 0x008500d4
/// Battery System: Thermistor Hot (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__THERMISTOR_HOT 0x008500d5
/// Battery System: Thermistor Cold (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__THERMISTOR_COLD 0x008500d6
/// Battery System: Thermistor Over Range (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__THERMISTOR_OVER_RANGE 0x008500d7
/// Battery System: Voltage Out Of Range (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__VOLTAGE_OUT_OF_RANGE 0x008500d8
/// Battery System: Current Out Of Range (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CURRENT_OUT_OF_RANGE 0x008500d9
/// Battery System: Current Not Regulated (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CURRENT_NOT_REGULATED 0x008500da
/// Battery System: Voltage Not Regulated (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__VOLTAGE_NOT_REGULATED 0x008500db
/// Battery System: Master Mode (DV)
#define USB_HID_USAGE__BATTERY_SYSTEM__MASTER_MODE 0x008500dc
/// Battery System: Charger Selector Support (SF)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGER_SELECTOR_SUPPORT 0x008500f0
/// Battery System: Charger Spec (SV)
#define USB_HID_USAGE__BATTERY_SYSTEM__CHARGER_SPEC 0x008500f1
/// Battery System: Level 2 (SF)
#define USB_HID_USAGE__BATTERY_SYSTEM__LEVEL_2 0x008500f2
/// Battery System: Level 3 (SF)
#define USB_HID_USAGE__BATTERY_SYSTEM__LEVEL_3 0x008500f3
static const struct usb_hid_usage_info {
uint32_t usage;
const char *name;
} usb_hid_usages[] = {
{0x00840001, "iName"},
{0x00840002, "Present Status"},
{0x00840003, "Changed Status"},
{0x00840004, "UPS"},
{0x00840005, "Power Supply"},
{0x00840010, "Battery System"},
{0x00840011, "Battery System Id"},
{0x00840012, "Battery"},
{0x00840013, "Battery Id"},
{0x00840014, "Charger"},
{0x00840015, "Charger Id"},
{0x00840016, "Power Converter"},
{0x00840017, "Power Converter Id"},
{0x00840018, "Outlet System"},
{0x00840019, "Outlet System Id"},
{0x0084001a, "Input"},
{0x0084001b, "Input Id"},
{0x0084001c, "Output"},
{0x0084001d, "Output Id"},
{0x0084001e, "Flow"},
{0x0084001f, "Flow Id"},
{0x00840020, "Outlet"},
{0x00840021, "Outlet Id"},
{0x00840022, "Gang"},
{0x00840023, "Gang Id"},
{0x00840024, "Power Summary"},
{0x00840025, "Power Summary Id"},
{0x00840030, "Voltage"},
{0x00840031, "Current"},
{0x00840032, "Frequency"},
{0x00840033, "Apparent Power"},
{0x00840034, "Active Power"},
{0x00840035, "Percent Load"},
{0x00840036, "Temperature"},
{0x00840037, "Humidity"},
{0x00840038, "Bad Count"},
{0x00840040, "Config Voltage"},
{0x00840041, "Config Current"},
{0x00840042, "Config Frequency"},
{0x00840043, "Config Apparent Power"},
{0x00840044, "Config Active Power"},
{0x00840045, "Config Percent Load"},
{0x00840046, "Config Temperature"},
{0x00840047, "Config Humidity"},
{0x00840050, "Switch On Control"},
{0x00840051, "Switch Off Control"},
{0x00840052, "Toggle Control"},
{0x00840053, "Low Voltage Transfer"},
{0x00840054, "High Voltage Transfer"},
{0x00840055, "Delay Before Reboot"},
{0x00840056, "Delay Before Startup"},
{0x00840057, "Delay Before Shutdown"},
{0x00840058, "Test"},
{0x00840059, "Module Reset"},
{0x0084005a, "Audible Alarm Control"},
{0x00840060, "Present"},
{0x00840061, "Good"},
{0x00840062, "Internal Failure"},
{0x00840063, "Voltag Out Of Range"},
{0x00840064, "Frequency Out Of Range"},
{0x00840065, "Overload"},
{0x00840066, "Over Charged"},
{0x00840067, "Over Temperature"},
{0x00840068, "Shutdown Requested"},
{0x00840069, "Shutdown Imminent"},
{0x0084006b, "Switch On/Off"},
{0x0084006c, "Switchable"},
{0x0084006d, "Used"},
{0x0084006e, "Boost"},
{0x0084006f, "Buck"},
{0x00840070, "Initialized"},
{0x00840071, "Tested"},
{0x00840072, "Awaiting Power"},
{0x00840073, "Communication Lost"},
{0x008400fd, "iManufacturer"},
{0x008400fe, "iProduct"},
{0x008400ff, "iSerialNumber"},
{0x00850001, "Smart Battery Battery Mode"},
{0x00850002, "Smart Battery Battery Status"},
{0x00850003, "Smart Battery Alarm Warning"},
{0x00850004, "Smart Battery Charger Mode"},
{0x00850005, "Smart Battery Charger Status"},
{0x00850006, "Smart Battery Charger Spec Info"},
{0x00850007, "Smart Battery Selector State"},
{0x00850008, "Smart Battery Selector Presets"},
{0x00850009, "Smart Battery Selector Info"},
{0x00850010, "Optional Mfg Function 1"},
{0x00850011, "Optional Mfg Function 2"},
{0x00850012, "Optional Mfg Function 3"},
{0x00850013, "Optional Mfg Function 4"},
{0x00850014, "Optional Mfg Function 5"},
{0x00850015, "Connection To SM Bus"},
{0x00850016, "Output Connection"},
{0x00850017, "Charger Connection"},
{0x00850018, "Battery Insertion"},
{0x00850019, "Use Next"},
{0x0085001a, "OK To Use"},
{0x0085001b, "Battery Supported"},
{0x0085001c, "Selector Revision"},
{0x0085001d, "Charging Indicator"},
{0x00850028, "Manufacturer Access"},
{0x00850029, "Remaining Capacity Limit"},
{0x0085002a, "Remaining Time Limit"},
{0x0085002b, "At Rate"},
{0x0085002c, "Capacity Mode"},
{0x0085002d, "Broadcast To Charger"},
{0x0085002e, "Primary Battery"},
{0x0085002f, "Charge Controller"},
{0x00850040, "Terminate Charge"},
{0x00850041, "Terminate Discharge"},
{0x00850042, "Below Remaining Capacity Limit"},
{0x00850043, "Remaining Time Limit Expired"},
{0x00850044, "Charging"},
{0x00850045, "Discharging"},
{0x00850046, "Fully Charged"},
{0x00850047, "Fully Discharged"},
{0x00850048, "Conditioning Flag"},
{0x00850049, "At Rate OK"},
{0x0085004a, "Smart Battery Error Code"},
{0x0085004b, "Need Replacement"},
{0x00850060, "At Rate Time To Full"},
{0x00850061, "At Rate Time To Empty"},
{0x00850062, "Average Current"},
{0x00850063, "Max Error"},
{0x00850064, "Relative State Of Charge"},
{0x00850065, "Absolute State Of Charge"},
{0x00850066, "Remaining Capacity"},
{0x00850067, "Full Charge Capacity"},
{0x00850068, "Run Time To Empty"},
{0x00850069, "Average Time To Empty"},
{0x0085006a, "Average Time To Full"},
{0x0085006b, "Cycle Count"},
{0x00850080, "Battery Pack Model Level"},
{0x00850081, "Internal Charge Controller"},
{0x00850082, "Primary Battery Support"},
{0x00850083, "Design Capacity"},
{0x00850084, "Specification Info"},
{0x00850085, "Manufacture Date"},
{0x00850086, "Serial Number"},
{0x00850087, "iManufacturer Name"},
{0x00850088, "iDevice Name"},
{0x00850089, "iDevice Chemistry"},
{0x0085008a, "Manufacturer Data"},
{0x0085008b, "Rechargable"},
{0x0085008c, "Warning Capacity Limit"},
{0x0085008d, "Capacity Granularity 1"},
{0x0085008e, "Capacity Granularity 2"},
{0x0085008f, "iOEM Information"},
{0x008500c0, "Inhibit Charge"},
{0x008500c1, "Enable Polling"},
{0x008500c2, "Reset To Zero"},
{0x008500d0, "AC Present"},
{0x008500d1, "Battery Present"},
{0x008500d2, "Power Fail"},
{0x008500d3, "Alarm Inhibited"},
{0x008500d4, "Thermistor Under Range"},
{0x008500d5, "Thermistor Hot"},
{0x008500d6, "Thermistor Cold"},
{0x008500d7, "Thermistor Over Range"},
{0x008500d8, "Voltage Out Of Range"},
{0x008500d9, "Current Out Of Range"},
{0x008500da, "Current Not Regulated"},
{0x008500db, "Voltage Not Regulated"},
{0x008500dc, "Master Mode"},
{0x008500f0, "Charger Selector Support"},
{0x008500f1, "Charger Spec"},
{0x008500f2, "Level 2"},
{0x008500f3, "Level 3"},
};
static const struct usb_hid_usage_range_info {
uint32_t usage_start;
uint32_t usage_end;
const char *name_prefix;
} usb_hid_usages_ranged[] = {
};
static int
usb_hid_usage_pages_cmp(const void *a, const void *b)
{
const uint16_t *key = a;
const struct usb_hid_usage_page_info *entry = b;
return (*key > entry->usage_page) - (*key < entry->usage_page);
}
static int
usb_hid_usages_cmp(const void *a, const void *b)
{
const uint32_t *key = a;
const struct usb_hid_usage_info *entry = b;
return (*key > entry->usage) - (*key < entry->usage);
}
static int
usb_hid_usages_ranged_cmp(const void *a, const void *b)
{
const uint32_t *key = a;
const struct usb_hid_usage_range_info *entry = b;
return (*key > entry->usage_end) - (*key < entry->usage_start);
}
static const char *
usb_hid_usage_page_to_string(uint16_t usage_page)
{
const struct usb_hid_usage_page_info *entry = bsearch(
&usage_page, usb_hid_usage_pages,
sizeof usb_hid_usage_pages / sizeof usb_hid_usage_pages[0],
sizeof usb_hid_usage_pages[0], usb_hid_usage_pages_cmp);
if (entry)
return entry->name;
static char buffer[128];
snprintf(buffer, sizeof buffer, "%04x", usage_page);
return buffer;
}
static const char *
usb_hid_usage_to_string(uint32_t usage)
{
static char buffer[128];
const struct usb_hid_usage_info *info = bsearch(
&usage, usb_hid_usages,
sizeof usb_hid_usages / sizeof usb_hid_usages[0],
sizeof usb_hid_usages[0], usb_hid_usages_cmp);
if (info)
return info->name;
const struct usb_hid_usage_range_info *range = bsearch(
&usage, usb_hid_usages_ranged,
sizeof usb_hid_usages_ranged / sizeof usb_hid_usages_ranged[0],
sizeof usb_hid_usages_ranged[0], usb_hid_usages_ranged_cmp);
if (range) {
snprintf(buffer, sizeof buffer,
"%s %u", range->name_prefix, (unsigned) (usage & 0xffff));
return buffer;
}
snprintf(buffer, sizeof buffer, "%04x", (unsigned) (usage & 0xffff));
return buffer;
}
static const char *
usb_hid_usage_to_string_full(uint32_t usage)
{
static char buffer[128];
snprintf(buffer, sizeof buffer, "%s: %s",
usb_hid_usage_page_to_string(usage >> 16),
usb_hid_usage_to_string(usage));
return buffer;
}
+992
View File
@@ -0,0 +1,992 @@
/*
* 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.
*
*/
// On Windows, vswprintf() interprets %s in the width of the format string,
// and %hs is not really compliant with any standard:
// https://devblogs.microsoft.com/oldnewthing/20190830-00/?p=102823
#ifdef _WIN32
#define __USE_MINGW_ANSI_STDIO
#endif
#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
#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)
{
#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;
}
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) {
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;
}
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 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 = {.info = &(struct hid_device_info) {
.path = "",
.manufacturer_string = L"Test",
.product_string = L"Test",
}};
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;
}