blob: d242aebc1a28699ae60ac82b0f005fef2f4a0367 [file] [log] [blame]
/*
* BRLTTY - A background process providing access to the console screen (when in
* text mode) for a blind person using a refreshable braille display.
*
* Copyright (C) 1995-2023 by The BRLTTY Developers.
*
* BRLTTY comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
#include "prologue.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include "program.h"
#include "cmdline.h"
#include "log.h"
#include "strfmt.h"
#include "parse.h"
#include "io_hid.h"
#include "hid_items.h"
#include "hid_inspect.h"
static int opt_matchUSBDevices;
static int opt_matchBluetoothDevices;
static char *opt_matchVendorIdentifier;
static char *opt_matchProductIdentifier;
static char *opt_matchManufacturerName;
static char *opt_matchProductDescription;
static char *opt_matchSerialNumber;
static char *opt_matchDeviceAddress;
static char *opt_matchDeviceName;
static int opt_showDeviceIdentifiers;
static int opt_showDeviceAddress;
static int opt_showDeviceName;
static int opt_showHostPath;
static int opt_showHostDevice;
static int opt_listItems;
static int opt_listReports;
static char *opt_readReport;
static char *opt_readFeature;
static char *opt_writeReport;
static char *opt_writeFeature;
static int opt_echoInput;
static char *opt_inputTimeout;
BEGIN_OPTION_TABLE(programOptions)
{ .word = "match-usb-devices",
.letter = 'u',
.setting.flag = &opt_matchUSBDevices,
.description = strtext("Filter for a USB device (the default if not ambiguous).")
},
{ .word = "match-bluetooth-devices",
.letter = 'b',
.setting.flag = &opt_matchBluetoothDevices,
.description = strtext("Filter for a Bluetooth device.")
},
{ .word = "match-vendor-identifier",
.letter = 'v',
.argument = strtext("identifier"),
.setting.string = &opt_matchVendorIdentifier,
.description = strtext("Match the vendor identifier (four hexadecimal digits).")
},
{ .word = "match-product-identifier",
.letter = 'p',
.argument = strtext("identifier"),
.setting.string = &opt_matchProductIdentifier,
.description = strtext("Match the product identifier (four hexadecimal digits).")
},
{ .word = "match-manufacturer-name",
.letter = 'm',
.argument = strtext("string"),
.setting.string = &opt_matchManufacturerName,
.description = strtext("Match the start of the manufacturer name (USB only).")
},
{ .word = "match-product-description",
.letter = 'd',
.argument = strtext("string"),
.setting.string = &opt_matchProductDescription,
.description = strtext("Match the start of the product description (USB only).")
},
{ .word = "match-serial-number",
.letter = 's',
.argument = strtext("string"),
.setting.string = &opt_matchSerialNumber,
.description = strtext("Match the start of the serial number (USB only).")
},
{ .word = "match-device-address",
.letter = 'a',
.argument = strtext("octets"),
.setting.string = &opt_matchDeviceAddress,
.description = strtext("Match the full device address (Bluetooth only - all six two-digit, hexadecimal octets separated by a colon [:]).")
},
{ .word = "match-device-name",
.letter = 'n',
.argument = strtext("string"),
.setting.string = &opt_matchDeviceName,
.description = strtext("Match the start of the device name (Bluetooth only).")
},
{ .word = "show-device-identifiers",
.letter = 'I',
.setting.flag = &opt_showDeviceIdentifiers,
.description = strtext("Show the vendor and product identifiers.")
},
{ .word = "show-device-address",
.letter = 'A',
.setting.flag = &opt_showDeviceAddress,
.description = strtext("Show the device address (USB serial number, Bluetooth device address, etc).")
},
{ .word = "show-device-name",
.letter = 'N',
.setting.flag = &opt_showDeviceName,
.description = strtext("Show the device name (USB manufacturer and/or product strings, Bluetooth device name, etc).")
},
{ .word = "show-host-path",
.letter = 'P',
.setting.flag = &opt_showHostPath,
.description = strtext("Show the host path (USB topology, Bluetooth host controller address, etc).")
},
{ .word = "show-host-device",
.letter = 'D',
.setting.flag = &opt_showHostDevice,
.description = strtext("Show the host device (usually its absolute path).")
},
{ .word = "list-items",
.letter = 'l',
.setting.flag = &opt_listItems,
.description = strtext("List the HID report descriptor's items.")
},
{ .word = "list-reports",
.letter = 'L',
.setting.flag = &opt_listReports,
.description = strtext("List each report's identifier and sizes.")
},
{ .word = "read-report",
.letter = 'r',
.argument = strtext("identifier"),
.setting.string = &opt_readReport,
.description = strtext("Read (get) an input report (two hexadecimal digits).")
},
{ .word = "read-feature",
.letter = 'R',
.argument = strtext("identifier"),
.setting.string = &opt_readFeature,
.description = strtext("Read (get) a feature report (two hexadecimal digits).")
},
{ .word = "write-report",
.letter = 'w',
.argument = strtext("bytes"),
.setting.string = &opt_writeReport,
.description = strtext("Write (set) an output report (see below)."),
},
{ .word = "write-feature",
.letter = 'W',
.argument = strtext("bytes"),
.setting.string = &opt_writeFeature,
.description = strtext("Write (set) a feature report (see below)."),
},
{ .word = "echo-input",
.letter = 'e',
.setting.flag = &opt_echoInput,
.description = strtext("Echo (in hexadecimal) input received from the device.")
},
{ .word = "input-timeout",
.letter = 't',
.argument = strtext("integer"),
.setting.string = &opt_inputTimeout,
.description = strtext("The input timeout (in seconds).")
},
END_OPTION_TABLE(programOptions)
static
BEGIN_USAGE_NOTES(usageNotes)
"When writing a report or feature, the bytes don't need to be, but can be, separated from one another by whitespace.",
"Each byte is either two hexadecimal digits or zero or more braille dot numbers within [square brackets].",
"A byte may optionally be followed by an asterisk [*] and a decimal count - if not specified, 1 is assumed.",
"The first byte is the report number - specify 00 for no report number.",
END_USAGE_NOTES
static FILE *outputStream;
static int outputError;
static int
canWriteOutput (void) {
if (outputError) return 0;
if (!ferror(outputStream)) return 1;
outputError = errno;
return 0;
}
static int writeBytesLine (
const char *format,
const unsigned char *from, size_t count,
...
) PRINTF(1, 4);
static int
writeBytesLine (const char *format, const unsigned char *from, size_t count, ...) {
const unsigned char *to = from + count;
char bytes[(count * 3) + 1];
STR_BEGIN(bytes, sizeof(bytes));
while (from < to) {
STR_PRINTF(" %02X", *from++);
}
STR_END;
char label[0X100];
{
va_list arguments;
va_start(arguments, count);
vsnprintf(label, sizeof(label), format, arguments);
va_end(arguments);
}
fprintf(outputStream, "%s:%s\n", label, bytes);
if (!canWriteOutput()) return 0;
fflush(outputStream);
return canWriteOutput();
}
static int
openDevice (HidDevice **device) {
HidFilter filter = {
.usb = {
.manufacturerName = opt_matchManufacturerName,
.productDescription = opt_matchProductDescription,
.serialNumber = opt_matchSerialNumber,
},
.bluetooth = {
.macAddress = opt_matchDeviceAddress,
.deviceName = opt_matchDeviceName,
},
.flags = {
.wantUSB = opt_matchUSBDevices,
.wantBluetooth = opt_matchBluetoothDevices,
},
};
int ok = hidSetFilterIdentifiers(
&filter, opt_matchVendorIdentifier, opt_matchProductIdentifier
);
if (!ok) return 0;
return hidOpenDeviceWithFilter(device, &filter);
}
static const HidItemsDescriptor *
getItems (HidDevice *device) {
const HidItemsDescriptor *items = hidGetItems(device);
if (!items) logMessage(LOG_ERR, "HID items not available");
return items;
}
static int
getReportSize (HidDevice *device, HidReportIdentifier identifier, HidReportSize *size) {
const HidItemsDescriptor *items = getItems(device);
if (!items) return 0;
return hidReportSize(items, identifier, size);
}
static void
logUnexpectedLength (
const char *what, HidReportIdentifier identifier,
size_t expected, size_t actual
) {
logMessage(LOG_WARNING,
"unexpected %s length: %02X:"
" Expected:%"PRIsize " Actual:%"PRIsize,
what, identifier, expected, actual
);
}
static int
performShowDeviceIdentifiers (HidDevice *device) {
HidDeviceIdentifier vendor;
HidDeviceIdentifier product;
if (!hidGetDeviceIdentifiers(device, &vendor, &product)) {
logMessage(LOG_WARNING, "vendor/product identifiers not available");
return 0;
}
fprintf(outputStream,
"Device Identifiers: %04X:%04X\n",
vendor, product
);
return 1;
}
static int
performShowDeviceAddress (HidDevice *device) {
const char *address = hidGetDeviceAddress(device);
if (!address) {
logMessage(LOG_WARNING, "device address not available");
return 0;
}
fprintf(outputStream, "Device Address: %s\n", address);
return 1;
}
static int
performShowDeviceName (HidDevice *device) {
const char *name = hidGetDeviceName(device);
if (!name) {
logMessage(LOG_WARNING, "device name not available");
return 0;
}
fprintf(outputStream, "Device Name: %s\n", name);
return 1;
}
static int
performShowHostPath (HidDevice *device) {
const char *path = hidGetHostPath(device);
if (!path) {
logMessage(LOG_WARNING, "host path not available");
return 0;
}
fprintf(outputStream, "Host Path: %s\n", path);
return 1;
}
static int
performShowHostDevice (HidDevice *device) {
const char *hostDevice = hidGetHostDevice(device);
if (!hostDevice) {
logMessage(LOG_WARNING, "host device not available");
return 0;
}
fprintf(outputStream, "Host Device: %s\n", hostDevice);
return 1;
}
static int
listItem (const char *line, void *data) {
fprintf(outputStream, "%s\n", line);
return canWriteOutput();
}
static int
performListItems (HidDevice *device) {
const HidItemsDescriptor *items = getItems(device);
if (!items) return 0;
hidListItems(items, listItem, NULL);
return 1;
}
static int
performListReports (HidDevice *device) {
const HidItemsDescriptor *items = getItems(device);
if (!items) return 0;
HidReports *reports = hidGetReports(items);
if (!reports) return 0;
for (unsigned int index=0; index<reports->count; index+=1) {
unsigned char identifier = reports->identifiers[index];
HidReportSize size;
char line[0X40];
STR_BEGIN(line, sizeof(line));
STR_PRINTF("Report %02X:", identifier);
if (hidReportSize(items, identifier, &size)) {
typedef struct {
const char *label;
const size_t value;
} SizeEntry;
const SizeEntry sizeTable[] = {
{ .value = size.input,
.label = "In",
},
{ .value = size.output,
.label = "Out",
},
{ .value = size.feature,
.label = "Ftr",
},
};
const SizeEntry *size = sizeTable;
const SizeEntry *end = size + ARRAY_COUNT(sizeTable);
while (size < end) {
if (size->value) {
STR_PRINTF(" %s:%"PRIsize, size->label, size->value);
}
size += 1;
}
}
STR_END;
fprintf(outputStream, "%s\n", line);
if (!canWriteOutput()) return 0;
}
free(reports);
return 1;
}
static int
isReportIdentifier (HidReportIdentifier *identifier, const char *string, unsigned char minimum) {
if (strlen(string) != 2) return 0;
char *end;
unsigned long int value = strtoul(string, &end, 0X10);
if (*end) return 0;
if (value > UINT8_MAX) return 0;
*identifier = value;
return 1;
}
static int
isReportDefined (
HidDevice *device, const char *what, HidReportIdentifier identifier,
HidReportSize *reportSize, size_t *size
) {
if (getReportSize(device, identifier, reportSize)) {
if (*size) {
return 1;
}
}
logMessage(LOG_ERR, "%s report not defined: %02X", what, identifier);
return 0;
}
static int
verifyRead (
HidDevice *device, const char *what, HidReportIdentifier identifier,
HidReportSize *reportSize, size_t *size
) {
int isDefined = isReportDefined(
device, what, identifier,
reportSize, size
);
return isDefined;
}
static HidReportIdentifier readReportIdentifier;
static int
parseReadReport (void) {
const char *operand = opt_readReport;
if (!*operand) return 1;
if (isReportIdentifier(&readReportIdentifier, operand, 0)) return 1;
logMessage(LOG_ERR, "invalid input report identifier: %s", operand);
return 0;
}
static int
performReadReport (HidDevice *device) {
HidReportIdentifier identifier = readReportIdentifier;
HidReportSize reportSize;
size_t *size = &reportSize.input;
int verified = verifyRead(
device, "input", identifier,
&reportSize, size
);
if (verified) {
size_t length = *size;
unsigned char report[length];
report[0] = identifier;
ssize_t result = hidGetReport(device, report, length);
if (result == -1) {
logSystemError("hidGetReport");
} else {
writeBytesLine("Input Report: %02X", report, result, identifier);
if (result == length) return 1;
logUnexpectedLength("report read", identifier, length, result);
}
}
return 0;
}
static HidReportIdentifier readFeatureIdentifier;
static int
parseReadFeature (void) {
const char *operand = opt_readFeature;
if (!*operand) return 1;
if (isReportIdentifier(&readFeatureIdentifier, operand, 1)) return 1;
logMessage(LOG_ERR, "invalid feature report identifier: %s", operand);
return 0;
}
static int
performReadFeature (HidDevice *device) {
HidReportIdentifier identifier = readFeatureIdentifier;
HidReportSize reportSize;
size_t *size = &reportSize.feature;
int verified = verifyRead(
device, "feature", identifier,
&reportSize, size
);
if (verified) {
size_t length = *size;
unsigned char feature[length];
feature[0] = identifier;
ssize_t result = hidGetFeature(device, feature, length);
if (result == -1) {
logSystemError("hidGetFeature");
} else {
writeBytesLine("Feature Report: %02X", feature, result, identifier);
if (result == length) return 1;
logUnexpectedLength("feature read", identifier, length, result);
}
}
return 0;
}
static int
isHexadecimal (unsigned char *digit, char character) {
const char string[] = {character, 0};
char *end;
long int value = strtol(string, &end, 0X10);
if (*end) {
logMessage(LOG_ERR, "invalid hexadecimal digit: %c", character);
return 0;
}
*digit = value;
return 1;
}
static int
parseBytes (
const char *bytes, const char *what, unsigned char *buffer,
size_t bufferSize, size_t *bufferUsed
) {
unsigned char *out = buffer;
const unsigned char *end = out + bufferSize;
if (*bytes) {
const char *in = bytes;
unsigned char byte = 0;
unsigned int count = 1;
enum {NEXT, HEX, DOTS, COUNT};
unsigned int state = NEXT;
while (*in) {
char character = *in++;
switch (state) {
case NEXT: {
if (iswspace(character)) continue;
if (character == '[') {
state = DOTS;
continue;
}
unsigned char digit;
if (!isHexadecimal(&digit, character)) return 0;
byte = digit << 4;
state = HEX;
continue;
}
case HEX: {
unsigned char digit;
if (!isHexadecimal(&digit, character)) return 0;
byte |= digit;
state = NEXT;
break;
}
case DOTS: {
if (character == ']') {
state = NEXT;
break;
}
if ((character < '1') || (character > '8')) {
logMessage(LOG_ERR, "invalid dot number: %c", character);
return 0;
}
unsigned char bit = 1 << (character - '1');
if (byte & bit) {
logMessage(LOG_ERR, "duplicate dot number: %c", character);
return 0;
}
byte |= bit;
continue;
}
case COUNT: {
if (iswspace(character)) break;
int digit = character - '0';
if ((digit < 0) || (digit > 9)) {
logMessage(LOG_ERR, "invalid count digit: %c", character);
return 0;
}
if (!digit) {
if (!count) {
logMessage(LOG_ERR, "first digit of count can't be 0");
return 0;
}
}
count *= 10;
count += digit;
if (!*in) break;
continue;
}
default:
logMessage(LOG_ERR, "unexpected bytes parser state: %u", state);
return 0;
}
if (state == COUNT) {
if (!count) {
logMessage(LOG_ERR, "missing count");
return 0;
}
state = NEXT;
} else if (*in == '*') {
in += 1;
state = COUNT;
count = 0;
continue;
}
while (count--) {
if (out == end) {
logMessage(LOG_ERR, "%s buffer too small", what);
return 0;
}
*out++ = byte;
}
byte = 0;
count = 1;
}
if (state != NEXT) {
logMessage(LOG_ERR, "incomplete %s specification", what);
return 0;
}
}
*bufferUsed = out - buffer;
return 1;
}
static int
verifyWrite (
HidDevice *device, const char *what, unsigned char *identifier,
HidReportSize *reportSize, size_t *expectedSize,
const unsigned char *buffer, size_t actualSize
) {
*identifier = buffer[0];
int isDefined = isReportDefined(
device, what, *identifier,
reportSize, expectedSize
);
if (!isDefined) return 0;
if (!*identifier) *expectedSize += 1;
if (actualSize != *expectedSize) {
logMessage(LOG_ERR,
"incorrect %s report size: %02X:"
" Expected:%"PRIsize " Actual:%"PRIsize,
what, *identifier, *expectedSize, actualSize
);
return 0;
}
return 1;
}
static unsigned char writeReportBuffer[0X1000];
static size_t writeReportLength;
static int
parseWriteReport (void) {
return parseBytes(
opt_writeReport, "output report", writeReportBuffer,
ARRAY_COUNT(writeReportBuffer), &writeReportLength
);
}
static int
performWriteReport (HidDevice *device) {
const unsigned char *report = writeReportBuffer;
size_t length = writeReportLength;
unsigned char identifier;
HidReportSize reportSize;
int verified = verifyWrite(
device, "output", &identifier,
&reportSize, &reportSize.output,
report, length
);
if (verified) {
writeBytesLine("Writing Report: %02X", report, length, identifier);
ssize_t result = hidSetReport(device, report, length);
if (result == -1) {
logSystemError("hidSetReport");
} else if (result == length) {
return 1;
} else {
logUnexpectedLength("report write", identifier, length, result);
}
}
return 0;
}
static unsigned char writeFeatureBuffer[0X1000];
static size_t writeFeatureLength;
static int
parseWriteFeature (void) {
return parseBytes(
opt_writeFeature, "feature report", writeFeatureBuffer,
ARRAY_COUNT(writeFeatureBuffer), &writeFeatureLength
);
}
static int
performWriteFeature (HidDevice *device) {
const unsigned char *feature = writeFeatureBuffer;
size_t length = writeFeatureLength;
unsigned char identifier;
HidReportSize reportSize;
int verified = verifyWrite(
device, "feature", &identifier,
&reportSize, &reportSize.feature,
feature, length
);
if (verified) {
writeBytesLine("Writing Feature: %02X", feature, length, identifier);
ssize_t result = hidSetFeature(device, feature, length);
if (result == -1) {
logSystemError("hidSetFeature");
} else if (result == length) {
return 1;
} else {
logUnexpectedLength("feature write", identifier, length, result);
}
}
return 0;
}
static int inputTimeout;
static int
parseInputTimeout (void) {
inputTimeout = 10;
static const int minimum = 1;
static const int maximum = 99;
if (!validateInteger(&inputTimeout, opt_inputTimeout, &minimum, &maximum)) {
logMessage(LOG_ERR, "invalid input timeout: %s", opt_inputTimeout);
return 0;
}
inputTimeout *= 1000;
return 1;
}
static int
performEchoInput (HidDevice *device) {
HidReportSize reportSize;
const size_t *inputSize = &reportSize.input;
unsigned char reportIdentifier = 0;
int hasReportIdentifiers = !getReportSize(device, reportIdentifier, &reportSize);
unsigned char buffer[0X1000];
size_t bufferSize = sizeof(buffer);
unsigned char *from = buffer;
unsigned char *to = from;
const unsigned char *end = from + bufferSize;
while (hidAwaitInput(device, inputTimeout)) {
ssize_t result = hidReadData(device, to, (end - to), 1000, 100);
if (result == -1) {
logMessage(LOG_ERR, "input error: %s", strerror(errno));
return 0;
}
to += result;
while (from < to) {
if (hasReportIdentifiers) {
reportIdentifier = *from;
if (!getReportSize(device, reportIdentifier, &reportSize)) {
logMessage(LOG_ERR, "input report not defined: %02X", reportIdentifier);
return 0;
}
}
if (!*inputSize) {
logMessage(LOG_ERR, "input report size is zero: %02X", reportIdentifier);
return 0;
}
size_t count = to - from;
if (*inputSize > count) {
if (from == buffer) {
logMessage(LOG_ERR,
"input report too large: %02X: %"PRIsize " > %"PRIsize,
reportIdentifier, *inputSize, count
);
return 0;
}
memmove(buffer, from, count);
from = buffer;
to = from + count;
break;
}
if (!writeBytesLine("Input Report", from, *inputSize)) return 0;
from += *inputSize;
}
}
return 1;
}
static int
parseOperands (void) {
typedef struct {
int (*parse) (void);
} OperandEntry;
static const OperandEntry operandTable[] = {
{ .parse = parseReadReport,
},
{ .parse = parseReadFeature,
},
{ .parse = parseWriteReport,
},
{ .parse = parseWriteFeature,
},
{ .parse = parseInputTimeout,
},
};
const OperandEntry *operand = operandTable;
const OperandEntry *end = operand + ARRAY_COUNT(operandTable);
while (operand < end) {
if (!operand->parse()) return 0;
operand += 1;
}
return 1;
}
static int
performActions (HidDevice *device) {
typedef struct {
int (*perform) (HidDevice *device);
unsigned char isFlag:1;
union {
char **string;
int *flag;
} option;
} ActionEntry;
static const ActionEntry actionTable[] = {
{ .perform = performShowDeviceIdentifiers,
.isFlag = 1,
.option.flag = &opt_showDeviceIdentifiers,
},
{ .perform = performShowDeviceAddress,
.isFlag = 1,
.option.flag = &opt_showDeviceAddress,
},
{ .perform = performShowDeviceName,
.isFlag = 1,
.option.flag = &opt_showDeviceName,
},
{ .perform = performShowHostPath,
.isFlag = 1,
.option.flag = &opt_showHostPath,
},
{ .perform = performShowHostDevice,
.isFlag = 1,
.option.flag = &opt_showHostDevice,
},
{ .perform = performListItems,
.isFlag = 1,
.option.flag = &opt_listItems,
},
{ .perform = performListReports,
.isFlag = 1,
.option.flag = &opt_listReports,
},
{ .perform = performReadReport,
.option.string = &opt_readReport,
},
{ .perform = performReadFeature,
.option.string = &opt_readFeature,
},
{ .perform = performWriteReport,
.option.string = &opt_writeReport,
},
{ .perform = performWriteFeature,
.option.string = &opt_writeFeature,
},
{ .perform = performEchoInput,
.isFlag = 1,
.option.flag = &opt_echoInput,
},
};
const ActionEntry *action = actionTable;
const ActionEntry *end = action + ARRAY_COUNT(actionTable);
while (action < end) {
int perform = 0;
if (action->isFlag) {
if (*action->option.flag) perform = 1;
} else {
if (**action->option.string) perform = 1;
}
if (perform) {
if (!action->perform(device)) return 0;
if (!canWriteOutput()) return 0;
}
action += 1;
}
return 1;
}
int
main (int argc, char *argv[]) {
{
const CommandLineDescriptor descriptor = {
.options = &programOptions,
.applicationName = "brltty-hid",
.usage = {
.purpose = strtext("Find HID devices, list report descriptors, read/write reports/features, or monitor input from a HID device."),
.notes = USAGE_NOTES(usageNotes),
}
};
PROCESS_OPTIONS(descriptor, argc, argv);
}
outputStream = stdout;
outputError = 0;
if (argc) {
logMessage(LOG_ERR, "too many parameters");
return PROG_EXIT_SYNTAX;
}
if (!parseOperands()) return PROG_EXIT_SYNTAX;
ProgramExitStatus exitStatus = PROG_EXIT_SUCCESS;
HidDevice *device = NULL;
if (!openDevice(&device)) {
exitStatus = PROG_EXIT_SYNTAX;
} else if (!device) {
logMessage(LOG_ERR, "device not found");
exitStatus = PROG_EXIT_SEMANTIC;
} else {
if (!performActions(device)) exitStatus = PROG_EXIT_FATAL;
hidCloseDevice(device);
}
if (outputError) {
logMessage(LOG_ERR, "output error: %s", strerror(outputError));
exitStatus = PROG_EXIT_FATAL;
}
return exitStatus;
}