usb-drivers/eizoctl.c
Přemysl Eric Janouch 9d619115be
Port eizoctltray to macOS
Also bump minimum CMake version for hidapi_ROOT,
and don't try to run the help2man Perl script in MSYS2.

AppKit is a very miserable thing.
2024-11-28 11:22:32 +01:00

1723 lines
50 KiB
C

/*
* eizoctl.c: EIZO Monitor Control
*
* This program stays independent of the liberty library
* in order to build on Windows.
*
* Copyright (c) 2024, 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.
*
*/
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <math.h>
#include <assert.h>
#include <getopt.h>
#include <hidapi.h>
#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "eizoctl"
#ifdef __OpenBSD__
#define hid_init hidapi_hid_init
#endif
#if defined __GNUC__
#define ATTRIBUTE_PRINTF(x, y) __attribute__((format(printf, x, y)))
#else
#define ATTRIBUTE_PRINTF(x, y)
#endif
static uint16_t
peek_u16le(const uint8_t *p)
{
return (uint16_t) p[1] << 8 | p[0];
}
static void
put_u16le(uint8_t *p, uint16_t value)
{
p[0] = value;
p[1] = value >> 8;
}
// --- Partial USB HID report descriptor parser --------------------------------
// This parser only needs to support EIZO monitors,
// which keep to a simple scheme, so it is appropriately simplified.
enum {
PARSER_REPORT_LIMIT = 256,
};
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,
};
struct parser_report {
uint32_t usage;
int32_t logical_minimum;
int32_t logical_maximum;
uint32_t report_size;
uint32_t report_id;
uint32_t report_count;
};
struct parser {
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 usage;
uint32_t usage_minimum;
uint32_t usage_maximum;
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)
{
bool array__variable = (flags >> 1) & 1;
bool absolute__relative = (flags >> 2) & 1;
bool non_volatile__volatile = (flags >> 7) & 1;
if (!array__variable) {
// Skip: This occurs at the end of the secondary descriptor.
return NULL;
}
if (absolute__relative)
return "Report item kind not supported: relative";
if (feature && non_volatile__volatile)
return "Report item kind not supported: volatile";
// We should really decide by the data length instead.
uint32_t usage = parser->local.usage;
if (usage < 0x10000)
usage = parser->global.usage_page << 16 | usage;
if (!usage)
return "zero Usage";
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";
if (global->report_size % 8) {
// Skip: This occurs at the end of the secondary descriptor.
return NULL;
}
struct parser_report *report = feature
? &parser->feature[global->report_id]
: &parser->input[global->report_id];
if (report->usage)
return "only one item per Report is supported";
report->usage = usage;
report->logical_minimum = global->logical_minimum;
report->logical_maximum = global->logical_maximum;
report->report_size = global->report_size;
report->report_id = global->report_id;
report->report_count = global->report_count;
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:
return "output items are not supported";
break; case PARSER_ITEM_TAG_MAIN_FEATURE:
err = parse_item_set(parser, u, true);
break; case PARSER_ITEM_TAG_MAIN_COLLECTION:
// Ignore for now.
// Top level Collections must be Application.
break; case PARSER_ITEM_TAG_MAIN_END_COLLECTION:
// Ignore for now.
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:
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:
// Note that reports can have multiple usages.
parser->local.usage = u;
break; case PARSER_ITEM_TAG_LOCAL_USAGE_MINIMUM:
parser->local.usage_minimum = u;
break; case PARSER_ITEM_TAG_LOCAL_USAGE_MAXIMUM:
parser->local.usage_maximum = u;
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;
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;
}
// --- EIZO monitor control ----------------------------------------------------
enum {
USB_VID_EIZO = 0x056d,
USB_USAGE_PAGE__MONITOR = 0x80,
USB_USAGE_PAGE__VESA_VIRTUAL_CONTROLS = 0x82,
USB_USAGE_PAGE_MONITOR_ID__MONITOR_CONTROL = 0x01,
EIZO_REPORT_ID_SECONDARY_DESCRIPTOR = 1,
EIZO_REPORT_ID_SET = 2,
EIZO_REPORT_ID_GET = 3,
EIZO_REPORT_ID_SET_LONG = 4,
EIZO_REPORT_ID_GET_LONG = 5,
EIZO_REPORT_ID_PIN_CODE = 6,
EIZO_REPORT_ID_RESULT = 7,
EIZO_REPORT_ID_SERIAL_PRODUCT = 8,
EIZO_REPORT_ID_PROFILE = 9,
EIZO_REPORT_ID_CRITICAL_SECTION = 10,
// I'm not sure of the interaction.
EIZO_REPORT_ID_CRITICAL_SET = 11,
EIZO_REPORT_ID_CRITICAL_GET = 12,
EIZO_REPORT_ID_CRITICAL_SET_LONG = 13,
EIZO_REPORT_ID_CRITICAL_GET_LONG = 14,
EIZO_REPORT_ID_COUNT = 10,
EIZO_SUBREPORT_COUNT = PARSER_REPORT_LIMIT,
EIZO_PROFILE_KEY_INPUT_PORTS = 0x53,
EIZO_PROFILE_KEY_USB_C_INPUT_PORTS = 0x61,
};
struct eizo_monitor {
uint16_t vid, pid; ///< USB device identification
hid_device *dev; ///< HID device handle
uint16_t pin_code; ///< Anti-race counter
char serial[9], product[17]; ///< Serial number, product name
struct eizo_profile_item {
uint8_t len;
uint8_t data[255];
} profile[256];
struct parser_report
report_input[EIZO_REPORT_ID_COUNT],
report_feature[EIZO_REPORT_ID_COUNT],
subreports_input[EIZO_SUBREPORT_COUNT],
subreports_feature[EIZO_SUBREPORT_COUNT];
// As a theme, we spend memory in order to limit code and dependencies.
char error[1024];
};
static bool
eizo_monitor_failf(struct eizo_monitor *m, const char *format, ...)
ATTRIBUTE_PRINTF(2, 3);
static bool
eizo_monitor_failf(struct eizo_monitor *m, const char *format, ...)
{
int len = *m->product
? snprintf(m->error, sizeof m->error, "%s %s: ", m->product, m->serial)
: snprintf(m->error, sizeof m->error, "%04x:%04x: ", m->vid, m->pid);
if (len >= 0) {
va_list ap;
va_start(ap, format);
(void) vsnprintf(m->error + len, sizeof m->error - len, format, ap);
va_end(ap);
}
return false;
}
static size_t
eizo_monitor_report_len(const struct parser_report *r)
{
assert(r->report_size % 8 == 0);
return r->report_size / 8 * r->report_count;
}
static bool
eizo_monitor_read_secondary_descriptor(struct eizo_monitor *m)
{
const struct parser_report *r =
&m->report_feature[EIZO_REPORT_ID_SECONDARY_DESCRIPTOR];
size_t lenr = 1 + r->report_size / 8 * r->report_count;
uint8_t buf[1024] = {r->report_id};
enum { HEADER_LEN = 1 + 2 + 2 };
if (sizeof buf < lenr)
return eizo_monitor_failf(m, "buffer too short");
if (hid_send_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"secondary descriptor Set_Feature failed: %ls", hid_error(m->dev));
uint8_t descriptor[8 << 10];
if (hid_get_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"secondary descriptor Get_Feature failed: %ls", hid_error(m->dev));
size_t offset = peek_u16le(&buf[1]);
if (offset)
return eizo_monitor_failf(m,
"secondary descriptor starts at an unexpected offset");
size_t descriptor_len = peek_u16le(&buf[3]);
if (descriptor_len > sizeof descriptor)
return eizo_monitor_failf(m, "secondary descriptor is too long A");
memcpy(descriptor + offset, &buf[HEADER_LEN], lenr - HEADER_LEN);
offset += lenr - HEADER_LEN;
while (offset < descriptor_len) {
if (hid_get_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"secondary descriptor Get_Feature failed: %ls",
hid_error(m->dev));
if (peek_u16le(&buf[1]) != offset)
return eizo_monitor_failf(m,
"secondary descriptor starts at an unexpected offset");
// We could also limit the amount we copy, but whatever.
if (offset + lenr - HEADER_LEN > sizeof descriptor)
return eizo_monitor_failf(m, "secondary descriptor is too long");
memcpy(descriptor + offset, &buf[HEADER_LEN], lenr - HEADER_LEN);
offset += lenr - HEADER_LEN;
}
#if DUMP_DESCRIPTORS
for (size_t i = 0; i < descriptor_len; i++)
printf("%02x ", descriptor[i]);
printf("\n");
#endif
struct parser parser = {};
const char *err = parse_descriptor(&parser, descriptor, descriptor_len);
if (err)
return eizo_monitor_failf(m, "secondary descriptor: %s", err);
memcpy(m->subreports_feature, parser.feature, sizeof m->subreports_feature);
memcpy(m->subreports_input, parser.input, sizeof m->subreports_input);
return true;
}
static bool
eizo_monitor_read_profile(struct eizo_monitor *m)
{
const struct parser_report *r = &m->report_feature[EIZO_REPORT_ID_PROFILE];
size_t lenr = 1 + eizo_monitor_report_len(r);
uint8_t buf[1024] = {r->report_id};
enum { HEADER_LEN = 1 + 2 + 2 };
if (sizeof buf < lenr)
return eizo_monitor_failf(m, "buffer too short");
if (hid_get_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"profile Get_Feature failed: %ls", hid_error(m->dev));
const uint8_t *p = buf + 1, *end = buf + lenr;
while (p + 2 <= end && p[0] != 0xff && p + 2 + p[1] <= end) {
struct eizo_profile_item *item = &m->profile[p[0]];
item->len = p[1];
memcpy(item->data, p + 2, item->len);
p += 2 + item->len;
}
return true;
}
static bool
eizo_monitor_open(struct eizo_monitor *m, const struct hid_device_info *info)
{
m->vid = info->vendor_id;
m->pid = info->product_id;
if (info->usage_page != USB_USAGE_PAGE__MONITOR ||
info->usage != USB_USAGE_PAGE_MONITOR_ID__MONITOR_CONTROL)
return eizo_monitor_failf(m, "unexpected HID usage");
// There can be more displays with the same VID/PID,
// and info does not contain the serial number to tell them apart.
hid_device *dev = hid_open_path(info->path);
if (!dev)
return eizo_monitor_failf(m, "%ls", hid_error(NULL));
uint8_t descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE] = {};
int len = hid_get_report_descriptor(dev, descriptor, sizeof descriptor);
if (len < 0) {
eizo_monitor_failf(m, "failed to read report descriptor");
goto out;
}
struct parser parser = {};
const char *err = parse_descriptor(&parser, descriptor, len);
if (err) {
eizo_monitor_failf(m, "failed to parse report descriptor: %s", err);
goto out;
}
for (unsigned id = 1; id < EIZO_REPORT_ID_COUNT; id++) {
if (parser.feature[id].usage == (0xff300000 | id))
continue;
eizo_monitor_failf(m, "EIZO HID report %u not supported", id);
goto out;
}
#if DUMP_DESCRIPTORS
for (size_t i = 0; i < (size_t) len; i++)
printf("%02x ", descriptor[i]);
printf("\n");
#endif
uint8_t pinbuf[3] = {EIZO_REPORT_ID_PIN_CODE, 0, 0};
if (hid_get_feature_report(dev, pinbuf, sizeof pinbuf) != sizeof pinbuf) {
eizo_monitor_failf(m, "failed to get PIN code: %ls", hid_error(dev));
goto out;
}
// ---
// Get it now, so that we have better error messages.
uint8_t idbuf[32] = {EIZO_REPORT_ID_SERIAL_PRODUCT};
size_t idlen = 1 + eizo_monitor_report_len(&parser.feature[idbuf[0]]);
if (sizeof idbuf < idlen) {
eizo_monitor_failf(m, "%s: %s",
"failed to get serial number and product", "report too long");
goto out;
}
if (hid_get_feature_report(dev, idbuf, idlen) != (int) idlen) {
eizo_monitor_failf(m, "%s: %ls",
"failed to get serial number and product", hid_error(dev));
goto out;
}
for (size_t i = idlen; --i && idbuf[i] == ' '; )
idbuf[i] = 0;
m->dev = dev;
m->pin_code = peek_u16le(&pinbuf[1]);
memcpy(m->serial, &idbuf[1], 8);
memcpy(m->product, &idbuf[9], idlen - 9);
memcpy(m->report_feature, parser.feature, sizeof m->report_feature);
memcpy(m->report_input, parser.input, sizeof m->report_input);
// Note that there are also "reduced models" without secondary descriptors.
// Those are all very old.
if (eizo_monitor_read_secondary_descriptor(m) &&
eizo_monitor_read_profile(m))
return true;
out:
hid_close(dev);
return false;
}
static void
eizo_monitor_close(struct eizo_monitor *m)
{
if (m->dev)
hid_close(m->dev);
*m = (struct eizo_monitor) {};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const struct parser_report *
eizo_monitor_subreport(const struct eizo_monitor *m, uint32_t usage)
{
// It doesn't really matter how efficient this is.
size_t len = sizeof m->subreports_feature / sizeof m->subreports_feature[0];
for (size_t i = 0; i < len; i++)
if (m->subreports_feature[i].usage == usage)
return &m->subreports_feature[i];
return NULL;
}
static bool
eizo_monitor_set(
struct eizo_monitor *m, uint32_t usage, const uint8_t *data, size_t len)
{
const struct parser_report *subr = eizo_monitor_subreport(m, usage),
*set1 = &m->report_feature[EIZO_REPORT_ID_SET],
*set2 = &m->report_feature[EIZO_REPORT_ID_SET_LONG];
if (!subr)
return eizo_monitor_failf(m, "set: usage not found: %#08x", usage);
size_t len1 = 1 + eizo_monitor_report_len(set1);
size_t len2 = 1 + eizo_monitor_report_len(set2);
// We need to encapsulate it.
uint8_t buf[1024] = {};
enum { HEADER_LEN = 1 + 4 + 2 };
if (len != eizo_monitor_report_len(subr) ||
sizeof buf < HEADER_LEN + len ||
sizeof buf < len1 || sizeof buf < len2)
return eizo_monitor_failf(m, "set: length check failed");
put_u16le(&buf[1], usage >> 16);
put_u16le(&buf[3], usage);
put_u16le(&buf[5], m->pin_code);
memcpy(&buf[7], data, len);
const struct parser_report *r = len1 >= HEADER_LEN + len ? set1 : set2;
size_t lenr = 1 + eizo_monitor_report_len(r);
buf[0] = r->report_id;
if (hid_send_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"set: Set_Feature failed: %ls", hid_error(m->dev));
// Don't use EIZO_REPORT_ID_RESULT now.
return true;
}
static bool
eizo_monitor_get(
struct eizo_monitor *m, uint32_t usage, uint8_t *data, size_t len)
{
const struct parser_report *subr = eizo_monitor_subreport(m, usage),
*get1 = &m->report_feature[EIZO_REPORT_ID_GET],
*get2 = &m->report_feature[EIZO_REPORT_ID_GET_LONG];
if (!subr)
return eizo_monitor_failf(m, "get: usage not found: %#08x", usage);
size_t len1 = 1 + eizo_monitor_report_len(get1);
size_t len2 = 1 + eizo_monitor_report_len(get2);
// We need to encapsulate it.
uint8_t buf[1024] = {};
enum { HEADER_LEN = 1 + 4 + 2 };
if (len != eizo_monitor_report_len(subr) ||
sizeof buf < HEADER_LEN + len ||
sizeof buf < len1 || sizeof buf < len2)
return eizo_monitor_failf(m, "get: length check failed");
put_u16le(&buf[1], usage >> 16);
put_u16le(&buf[3], usage);
put_u16le(&buf[5], m->pin_code);
const struct parser_report *r = len1 >= HEADER_LEN + len ? get1 : get2;
size_t lenr = 1 + eizo_monitor_report_len(r);
buf[0] = r->report_id;
if (hid_send_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"get: Set_Feature failed: %ls", hid_error(m->dev));
if (hid_get_feature_report(m->dev, buf, lenr) < 0)
return eizo_monitor_failf(m,
"get: Get_Feature failed: %ls", hid_error(m->dev));
if ((uint32_t) (peek_u16le(&buf[1]) << 16 | peek_u16le(&buf[3])) != usage ||
peek_u16le(&buf[5]) != m->pin_code)
return eizo_monitor_failf(m, "get: invalid result");
memcpy(data, buf + HEADER_LEN, len);
return true;
}
// --- EIZO monitor utilities --------------------------------------------------
enum {
DSUB1 = 0x100,
DSUB2 = 0x101,
DVI1 = 0x200,
DVI2 = 0x201,
DP1 = 0x300,
DP2 = 0x301,
HDMI1 = 0x400,
HDMI2 = 0x401,
};
static const char
// USB-C maps to a DisplayPort port, if present.
*g_port_names_usb_c[] = {"USB-C", "USBC", NULL},
**g_port_names[] = {
NULL,
(const char *[]) {"D-Sub", "DSub", "VGA", NULL},
(const char *[]) {"DVI", NULL},
(const char *[]) {"DisplayPort", "DP", NULL},
(const char *[]) {"HDMI", NULL},
};
// Monitors may or may not report these in their profile.
static const struct eizo_product {
const char *product;
const uint16_t *ports;
} g_products[] = {
{"CG243W", (const uint16_t[]) {DVI1, DVI2, DP1, 0}},
{"CG223W", (const uint16_t[]) {DVI1, DVI2, DP1, 0}},
{"CG245W", (const uint16_t[]) {DVI1, DVI2, DP1, 0}},
{"CG275W", (const uint16_t[]) {DVI1, DP1, DP2, 0}},
{"SX2462W", (const uint16_t[]) {DVI1, DVI2, DP1, 0}},
{"SX2262W", (const uint16_t[]) {DVI1, DVI2, DP1, 0}},
{"SX2762W", (const uint16_t[]) {DVI1, DP1, DP2, 0}},
{"S2232W", (const uint16_t[]) {DVI1, DSUB1, 0}},
{"S2432W", (const uint16_t[]) {DVI1, DSUB1, 0}},
{"S2242W", (const uint16_t[]) {DVI1, DSUB1, 0}},
{"S2233W", (const uint16_t[]) {DP1, DVI1, DSUB1, 0}},
{"S2433W", (const uint16_t[]) {DP1, DVI1, DSUB1, 0}},
{"S2243W", (const uint16_t[]) {DP1, DVI1, DSUB1, 0}},
{"EV2333W", (const uint16_t[]) {DP1, DVI1, DSUB1, 0}},
{"EV2334W", (const uint16_t[]) {HDMI1, DVI1, DSUB1, 0}},
{"EV2436W", (const uint16_t[]) {DP1, DVI1, DSUB1, 0}},
{"EV3237", (const uint16_t[]) {DVI1, DP1, DP2, HDMI1, 0}},
{"EV2450", (const uint16_t[]) {DSUB1, DVI1, DP1, HDMI1, 0}},
{"EV2455", (const uint16_t[]) {DSUB1, DVI1, DP1, HDMI1, 0}},
{"EV2750", (const uint16_t[]) {DVI1, DP1, HDMI1, 0}},
{"EV2451", (const uint16_t[]) {DSUB1, DVI1, DP1, HDMI1, 0}},
{"EV2456", (const uint16_t[]) {DSUB1, DVI1, DP1, HDMI1, 0}},
{"EV2457", (const uint16_t[]) {DVI1, DP1, HDMI1, 0}},
{"EV2480", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2485", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2490", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2495", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2780", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2795", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV3895", (const uint16_t[]) {DP1, DP2, HDMI1, HDMI2, 0}},
{"CG3145", (const uint16_t[]) {DP1, DP2, HDMI1, HDMI2, 0}},
{"CG3146", (const uint16_t[]) {
0xa00, 0xa01, 0xa02, 0xa03, 0xa10, 0xa11, 0xa20, DP1, HDMI1, 0}},
{"EV2785", (const uint16_t[]) {DP1, DP2, HDMI1, HDMI2, 0}},
{"EV3285", (const uint16_t[]) {DP1, DP2, HDMI1, HDMI2, 0}},
{"CG319X", (const uint16_t[]) {DP1, DP2, HDMI1, HDMI2, 0}},
{"CG279X", (const uint16_t[]) {DVI1, DP1, DP2, HDMI1, 0}},
{"CG2700X", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"CG2700S", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"CS2731", (const uint16_t[]) {DVI1, DP1, DP2, HDMI1, 0}},
{"CS2410", (const uint16_t[]) {DVI1, DP1, HDMI1, 0}},
{"CS2740", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2760", (const uint16_t[]) {DVI1, DP1, DP2, HDMI1, 0}},
{"EV2360", (const uint16_t[]) {DSUB1, DP1, HDMI1, 0}},
{"EV2460", (const uint16_t[]) {DSUB1, DVI1, DP1, HDMI1, 0}},
{"EV2781", (const uint16_t[]) {DP1, DP2, HDMI1, 0}},
{"EV2736W", (const uint16_t[]) {DP1, DVI1, 0}},
{}
};
static const uint16_t *
eizo_ports_by_product_name(const char *product)
{
for (size_t i = 0; g_products[i].product; i++)
if (!strcmp(g_products[i].product, product))
return g_products[i].ports;
return NULL;
}
// Match port names case-insensitively, with an optional one-based suffix.
static bool
eizo_port_by_name_in_group(const char *name, const char **group, uint8_t *index)
{
for (; *group; group++) {
size_t len = strlen(*group);
if (strncasecmp(*group, name, len))
continue;
if (!*(name += len))
return true;
char *end = NULL;
errno = 0;
long n = strtol(name, &end, 10);
if (errno || *end || n < 1 || n > 0x100)
return false;
*index = --n;
return true;
}
return false;
}
static uint16_t
eizo_port_by_name(const char *name)
{
char *end = NULL;
errno = 0;
long n = strtol(name, &end, 16);
if (!errno && !*end && n >= 0x100 && n <= UINT16_MAX)
return n;
uint8_t index = 0;
for (size_t i = 1; i < sizeof g_port_names / sizeof g_port_names[0]; i++)
if (eizo_port_by_name_in_group(name, g_port_names[i], &index))
return i * 0x100 | index;
return index;
}
static char *
eizo_port_to_name(uint16_t port)
{
const char *stem = NULL;
uint16_t group = port >> 8, number = port & 0xff;
if (group && group < sizeof g_port_names / sizeof g_port_names[0])
stem = g_port_names[group][0];
static char buffer[32] = "";
if (!stem)
snprintf(buffer, sizeof buffer, "%x", port);
else if (!number)
snprintf(buffer, sizeof buffer, "%s", stem);
else
snprintf(buffer, sizeof buffer, "%s %d", stem, number);
return buffer;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum {
EIZO_USAGE_BRIGHTNESS = 0x00820010,
EIZO_USAGE_INPUT_PORT = 0xff010048,
EIZO_USAGE_RESTART = 0xff0200f4,
};
static bool
eizo_get_brightness(struct eizo_monitor *m, double *brightness)
{
const struct parser_report *subr =
eizo_monitor_subreport(m, EIZO_USAGE_BRIGHTNESS);
if (!subr)
return eizo_monitor_failf(m, "missing HID usage");
// NOTE: this oddly doesn't work when there's no signal.
uint8_t buf[2] = {};
if (!eizo_monitor_get(m, EIZO_USAGE_BRIGHTNESS, buf, sizeof buf))
return false;
*brightness = (double) peek_u16le(buf) / subr->logical_maximum;
return true;
}
static bool
eizo_set_brightness(struct eizo_monitor *m, double brightness)
{
const struct parser_report *subr =
eizo_monitor_subreport(m, EIZO_USAGE_BRIGHTNESS);
if (!subr)
return eizo_monitor_failf(m, "missing HID usage");
if (brightness < 0)
brightness = 0;
if (brightness > 1)
brightness = 1;
uint8_t buf[2] = {};
put_u16le(buf, subr->logical_maximum * brightness);
return eizo_monitor_set(m, EIZO_USAGE_BRIGHTNESS, buf, sizeof buf);
}
static bool
eizo_get_input_port(struct eizo_monitor *m, uint16_t *port)
{
const struct parser_report *subr =
eizo_monitor_subreport(m, EIZO_USAGE_INPUT_PORT);
if (!subr)
return eizo_monitor_failf(m, "missing HID usage");
uint8_t buf[2] = {};
if (!eizo_monitor_get(m, EIZO_USAGE_INPUT_PORT, buf, sizeof buf))
return false;
*port = peek_u16le(buf);
return true;
}
static void
eizo_get_input_ports(struct eizo_monitor *m, uint16_t *ports, size_t size)
{
struct eizo_profile_item *item = &m->profile[EIZO_PROFILE_KEY_INPUT_PORTS];
if (item->len) {
for (size_t i = 0; i < size && i < item->len / 4; i++)
ports[i] = peek_u16le(item->data + i * 4);
} else {
const uint16_t *db = eizo_ports_by_product_name(m->product);
for (size_t i = 0; i < size && db && db[i]; i++)
ports[i] = db[i];
}
}
static uint16_t
eizo_resolve_port(struct eizo_monitor *m, const char *port)
{
uint8_t usb_c_index = 0;
if (eizo_port_by_name_in_group(port, g_port_names_usb_c, &usb_c_index)) {
struct eizo_profile_item *item =
&m->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS];
if (item->len / 2 > usb_c_index)
return peek_u16le(item->data + usb_c_index * 2);
}
return eizo_port_by_name(port);
}
static bool
eizo_set_input_port(struct eizo_monitor *m, uint16_t port)
{
const struct parser_report *subr =
eizo_monitor_subreport(m, EIZO_USAGE_INPUT_PORT);
if (!subr)
return eizo_monitor_failf(m, "missing HID usage");
uint8_t buf[2] = {};
put_u16le(buf, port);
return eizo_monitor_set(m, EIZO_USAGE_INPUT_PORT, buf, sizeof buf);
}
static bool
eizo_restart(struct eizo_monitor *m)
{
const struct parser_report *subr =
eizo_monitor_subreport(m, EIZO_USAGE_RESTART);
if (!subr)
return eizo_monitor_failf(m, "missing HID usage");
uint8_t buf[1] = {};
return eizo_monitor_set(m, EIZO_USAGE_RESTART, buf, 1);
}
// --- Main --------------------------------------------------------------------
static bool
eizo_watch(struct eizo_monitor *m)
{
uint8_t buf[1024] = {};
int res = 0;
while (true) {
if ((res = hid_read(m->dev, buf, sizeof buf)) < 0)
return eizo_monitor_failf(m, "watch: %ls", hid_error(m->dev));
if (buf[0] != EIZO_REPORT_ID_GET &&
buf[0] != EIZO_REPORT_ID_GET_LONG) {
printf("Unknown report ID\n");
continue;
}
uint16_t page = peek_u16le(&buf[1]), id = peek_u16le(&buf[3]);
uint32_t usage = page << 16 | id;
printf("%08x", usage);
const struct parser_report *r = eizo_monitor_subreport(m, usage);
if (!r) {
printf(" unknown usage\n");
continue;
}
size_t rlen = r->report_size / 8 * r->report_count;
if ((size_t) res < 7 + rlen) {
printf(" received data too short\n");
continue;
}
if (r->report_size == 16)
for (size_t i = 0; i + 1 < rlen; i += 2)
printf(" %04x", peek_u16le(&buf[7 + i]));
else
for (size_t i = 0; i < rlen; i++)
printf(" %02x", buf[7 + i]);
printf("\n");
}
}
typedef void (*print_fn)(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static int
run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
{
const char *name = argv[0];
const char *usage = "Usage: %s [--brightness [+-]BRIGHTNESS] [--input NAME]"
" [--restart] [--events]\n";
static struct option opts[] = {
{"input", required_argument, NULL, 'i'},
{"brightness", required_argument, NULL, 'b'},
{"restart", no_argument, NULL, 'r'},
{"events", no_argument, NULL, 'e'},
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'},
{}
};
double brightness = NAN;
bool relative = false, restart = false, events = false;
const char *port = NULL;
int c = 0;
while ((c = getopt_long(argc, argv, "b:i:h", opts, NULL)) != -1)
switch (c) {
case 'b':
relative = *optarg == '+' || *optarg == '-';
if (sscanf(optarg, "%lf", &brightness) && isfinite(brightness))
break;
error("Invalid value: %s\n", optarg);
error(usage, name);
return 1;
case 'i':
port = optarg;
break;
case 'r':
restart = true;
break;
case 'e':
events = true;
break;
case 'h':
output(usage, name);
return 0;
case 'V':
output(PROGRAM_NAME " " PROGRAM_VERSION "\n");
return 0;
default:
error("Unknown option\n");
error(usage, name);
return 1;
}
argc -= optind;
argv += optind;
if (argc != 0) {
error(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()) {
error("%ls\n", hid_error(NULL));
return 1;
}
// It should be possible to choose a particular monitor,
// but it is generally more useful to operate on all of them.
struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0), *p = devs;
for (; p; p = p->next) {
struct eizo_monitor m = {};
if (!eizo_monitor_open(&m, p)) {
error("%s\n", m.error);
continue;
}
if (isfinite(brightness)) {
double prev = 0.;
if (!eizo_get_brightness(&m, &prev)) {
error("Failed to get brightness: %s\n", m.error);
} else {
double next = relative ? brightness + prev : brightness;
if (!eizo_set_brightness(&m, next))
error("Failed to set brightness: %s\n", m.error);
else if (verbose)
output("%s %s: brightness: %.2f -> %.2f\n",
m.product, m.serial, prev, next);
}
}
if (port) {
uint16_t prev = 0;
uint16_t next = eizo_resolve_port(&m, port);
if (!eizo_get_input_port(&m, &prev)) {
error("Failed to get input port: %s\n", m.error);
} else if (!strcmp(port, "?")) {
output("%s %s: input: %s\n",
m.product, m.serial, eizo_port_to_name(prev));
} else if (!next) {
error("Failed to resolve port name: %s\n", port);
} else {
if (!eizo_set_input_port(&m, next))
error("Failed to set input port: %s\n", m.error);
else if (verbose)
output("%s %s: input: %s -> %s\n",
m.product, m.serial, eizo_port_to_name(prev), port);
}
}
if (restart) {
if (!eizo_restart(&m))
error("Failed to restart: %s\n", m.error);
else if (verbose)
output("%s %s: restart\n", m.product, m.serial);
}
if (events) {
if (!verbose)
error("Watching events is not possible in this mode\n");
else if (!eizo_watch(&m))
error("%s\n", m.error);
}
eizo_monitor_close(&m);
}
hid_free_enumeration(devs);
return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#if !defined TRAY
static void
stdio_output(const char *format, ...)
{
va_list ap;
va_start(ap, format);
vfprintf(stdout, format, ap);
va_end(ap);
}
static void
stdio_error(const char *format, ...)
{
va_list ap;
va_start(ap, format);
vfprintf(stderr, format, ap);
va_end(ap);
}
int
main(int argc, char *argv[])
{
return run(argc, argv, stdio_output, stdio_error, true);
}
// --- Windows -----------------------------------------------------------------
#elif defined _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <powrprof.h>
#include <wchar.h>
static wchar_t *
message_printf(const char *format, va_list ap)
{
size_t format_wide_len = mbstowcs(NULL, format, 0) + 1;
wchar_t *format_wide = calloc(format_wide_len, sizeof *format_wide);
if (!format_wide)
return NULL;
mbstowcs(format_wide, format, format_wide_len);
int message_len = vswprintf(NULL, 0, format_wide, ap) + 1;
wchar_t *message = calloc(message_len, sizeof *message);
if (message_len > 0 && message)
vswprintf(message, message_len, format_wide, ap);
free(format_wide);
return message;
}
static void message_output(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static void message_error(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static void
message_output(const char *format, ...)
{
va_list ap;
va_start(ap, format);
wchar_t *message = message_printf(format, ap);
va_end(ap);
if (message) {
MessageBox(
NULL, message, NULL, MB_ICONINFORMATION | MB_OK | MB_APPLMODAL);
free(message);
}
}
static void
message_error(const char *format, ...)
{
va_list ap;
va_start(ap, format);
wchar_t *message = message_printf(format, ap);
va_end(ap);
if (message) {
MessageBox(NULL, message, NULL, MB_ICONERROR | MB_OK | MB_APPLMODAL);
free(message);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct {
HWND hwnd;
} g;
enum {
IDM_QUIT = 1,
IDM_BRIGHTER,
IDM_DARKER,
IDM_INPUT_0,
};
static void
append_monitor(struct eizo_monitor *m, HMENU menu, UINT_PTR base)
{
wchar_t buf[256] = L"";
snwprintf(buf, sizeof buf, L"%s %s", m->product, m->serial);
AppendMenu(menu, MF_STRING | MF_GRAYED, IDM_QUIT, buf);
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
double brightness = 0;
(void) eizo_get_brightness(m, &brightness);
UINT flags_brighter = MF_STRING;
if (brightness == 1)
flags_brighter |= MF_GRAYED;
UINT flags_darker = MF_STRING;
if (brightness == 0)
flags_darker |= MF_GRAYED;
// XXX: These are some stupid choices.
AppendMenu(menu, flags_brighter, base + IDM_BRIGHTER, L"Brighter");
AppendMenu(menu, flags_darker, base + IDM_DARKER, L"Darker");
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
uint16_t ports[16] = {0}, current = 0;
eizo_get_input_ports(m, ports, sizeof ports / sizeof ports[0] - 1);
(void) eizo_get_input_port(m, &current);
if (!ports[0])
ports[0] = current;
// USB-C ports are a bit tricky, they only need to be /displayed/ as such.
struct eizo_profile_item *item =
&m->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS];
for (size_t i = 0; ports[i]; i++) {
uint8_t usb_c = 0;
for (size_t u = 0; u < item->len / 2; u++)
if (ports[i] == peek_u16le(item->data + u * 2))
usb_c = u + 1;
if (!usb_c)
snwprintf(buf, sizeof buf, L"%s", eizo_port_to_name(ports[i]));
else if (usb_c == 1)
snwprintf(buf, sizeof buf, L"%s", g_port_names_usb_c[0]);
else
snwprintf(buf, sizeof buf, L"%s %u", g_port_names_usb_c[0], usb_c);
UINT flags = MF_STRING;
if (ports[i] == current)
flags |= MF_CHECKED;
AppendMenu(menu, flags, base + IDM_INPUT_0 + ports[i], buf);
}
}
static bool
process_any_power_request(void)
{
if (GetAsyncKeyState(VK_CONTROL) & 0x8000) {
if (ExitWindowsEx(EWX_POWEROFF,
SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_FLAG_PLANNED))
return true;
message_error("Shut down request failed.");
return false;
}
if (GetAsyncKeyState(VK_SHIFT) & 0x8000) {
if (SetSuspendState(FALSE, FALSE, FALSE))
return true;
message_error("Suspend request failed.");
return false;
}
return true;
}
static void
show_menu(void)
{
struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0);
size_t monitors_size = 0, monitors_len = 0;
for (struct hid_device_info *p = devs; p; p = p->next)
monitors_size++;
HMENU popup = CreatePopupMenu();
struct eizo_monitor *monitors = calloc(monitors_size, sizeof *monitors);
if (monitors) {
for (struct hid_device_info *p = devs; p; p = p->next) {
struct eizo_monitor *m = monitors + monitors_len;
if (!eizo_monitor_open(m, p))
continue;
UINT_PTR base = 0x1000 * ++monitors_len;
append_monitor(m, popup, base);
AppendMenu(popup, MF_SEPARATOR, 0, NULL);
}
}
if (!monitors_len) {
AppendMenu(popup, MF_STRING | MF_GRAYED, 0, L"No monitors found");
AppendMenu(popup, MF_SEPARATOR, 0, NULL);
}
AppendMenu(popup, MF_STRING, IDM_QUIT, L"&Quit");
UINT flags = TPM_NONOTIFY | TPM_RETURNCMD | TPM_RIGHTBUTTON;
if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0)
flags |= TPM_RIGHTALIGN;
else
flags |= TPM_LEFTALIGN;
// When invoked using the keyboard,
// the cursor gets automatically warped to where we want it.
POINT pt = {};
GetCursorPos(&pt);
SetForegroundWindow(g.hwnd);
UINT id = TrackPopupMenuEx(popup, flags, pt.x, pt.y, g.hwnd, NULL);
UINT id_monitor = id / 0x1000;
if (id == IDM_QUIT) {
PostQuitMessage(0);
} else if (id_monitor && id_monitor <= monitors_len) {
struct eizo_monitor *m = &monitors[--id_monitor];
id = id % 0x1000;
double brightness = 0.;
if (id >= IDM_INPUT_0) {
if (process_any_power_request())
eizo_set_input_port(m, id - IDM_INPUT_0);
} else if (id == IDM_BRIGHTER) {
if (eizo_get_brightness(m, &brightness))
eizo_set_brightness(m, min(1., brightness + .1));
} else if (id == IDM_DARKER) {
if (eizo_get_brightness(m, &brightness))
eizo_set_brightness(m, max(0., brightness - .1));
}
}
DestroyMenu(popup);
while (monitors_len--)
eizo_monitor_close(&monitors[monitors_len]);
free(monitors);
}
static LRESULT CALLBACK
window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) {
case WM_APP + 0:
// We get the mouse events synthesized.
switch (LOWORD(lParam)) {
case WM_LBUTTONUP:
case WM_RBUTTONUP:
show_menu();
}
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static bool
enable_shutdown_privilege(void)
{
HANDLE hToken = NULL;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
return FALSE;
TOKEN_PRIVILEGES tkp = {
.PrivilegeCount = 1,
.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED,
};
bool result =
LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0].Luid) &&
AdjustTokenPrivileges(
hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES) NULL, 0) &&
GetLastError() != ERROR_NOT_ALL_ASSIGNED;
CloseHandle(hToken);
return result;
}
int WINAPI
wWinMain(
HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
{
(void) hPrevInstance;
(void) nCmdShow;
// Not having a console window is desirable for automation.
int argc = 0;
LPWSTR *argv = CommandLineToArgvW(pCmdLine, &argc);
if (*pCmdLine) {
char **mbargv = calloc(argc + 1, sizeof *mbargv);
mbargv[0] = calloc(MAX_PATH + 1, sizeof *mbargv[0]);
GetModuleFileNameA(hInstance, mbargv[0], MAX_PATH);
for (int i = 0; i < argc; i++) {
// On conversion error, this ends up being an empty string.
size_t len = wcstombs(NULL, argv[i], 0) + 1;
char *mb = mbargv[i + 1] = calloc(len, sizeof *mb);
wcstombs(mb, argv[i], len);
}
return run(argc + 1, mbargv, message_output, message_error, false);
}
LocalFree(argv);
(void) enable_shutdown_privilege();
// This is actually not needed, so we might even leave it out.
if (hid_init()) {
message_error("%ls", hid_error(NULL));
return 1;
}
WNDCLASSEX wc = {
.cbSize = sizeof wc,
.lpfnWndProc = window_proc,
.hInstance = hInstance,
.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1)),
.hCursor = LoadCursor(NULL, IDC_ARROW),
.hbrBackground = GetSysColorBrush(COLOR_3DFACE),
.lpszClassName = TEXT(PROGRAM_NAME),
};
if (!RegisterClassEx(&wc))
return 1;
// We need a window, but it can stay hidden.
g.hwnd = CreateWindowEx(WS_EX_CONTROLPARENT,
wc.lpszClassName, TEXT(PROGRAM_NAME), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL);
NOTIFYICONDATA nid = {
.cbSize = sizeof nid,
.hWnd = g.hwnd,
.uID = 0,
.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP,
.uCallbackMessage = WM_APP + 0,
// TODO(p): LoadIconMetric is suggested for high-DPI displays.
.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1)),
.szTip = TEXT(PROGRAM_NAME),
};
if (!Shell_NotifyIcon(NIM_ADD, &nid)) {
message_error("Failed to add notification area icon.");
return 1;
}
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
(void) Shell_NotifyIcon(NIM_DELETE, &nid);
return msg.wParam;
}
// --- macOS -------------------------------------------------------------------
#elif defined __APPLE__
#include <AppKit/AppKit.h>
#include <AppKit/NSStatusBar.h>
#include <Foundation/Foundation.h>
static void message_output(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static void message_error(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static void
message_output(const char *format, ...)
{
va_list ap;
va_start(ap, format);
NSString *message = [[NSString alloc]
initWithFormat:[NSString stringWithUTF8String: format] arguments:ap];
va_end(ap);
NSAlert *alert = [NSAlert new];
[alert setMessageText:message];
[alert setAlertStyle:NSAlertStyleInformational];
// XXX: How to make the OK button the first responder?
[alert addButtonWithTitle:@"OK"];
[NSApp activate];
[alert.window makeKeyAndOrderFront:nil];
[alert runModal];
}
static void
message_error(const char *format, ...)
{
va_list ap;
va_start(ap, format);
NSString *message = [[NSString alloc]
initWithFormat:[NSString stringWithUTF8String: format] arguments:ap];
va_end(ap);
NSAlert *alert = [NSAlert new];
[alert setMessageText:message];
[alert setAlertStyle:NSAlertStyleCritical];
[alert addButtonWithTitle:@"OK"];
[NSApp activate];
[alert.window makeKeyAndOrderFront:nil];
[alert runModal];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Monitor provides reference counting, and enables use of NSArray.
@interface Monitor : NSObject
@property (assign, nonatomic) struct eizo_monitor *monitor;
- (instancetype)initWithMonitor:(struct eizo_monitor *)monitor;
@end
@implementation Monitor
- (instancetype)initWithMonitor:(struct eizo_monitor *)monitor {
if (self = [super init]) {
_monitor = monitor;
}
return self;
}
- (void)dealloc {
if (_monitor) {
eizo_monitor_close(_monitor);
free(_monitor);
_monitor = NULL;
}
}
@end
@interface ApplicationDelegate
: NSObject <NSApplicationDelegate, NSMenuDelegate>
@property (strong, nonatomic) NSStatusItem *statusItem;
@property (strong, nonatomic) NSMutableArray<Monitor *> *monitors;
@end
@implementation ApplicationDelegate
- (Monitor *)getMonitorFrom:(NSControl *)control {
NSInteger index = control.tag / 0x1000;
if (!self.monitors || index < 0 || index >= self.monitors.count)
return nil;
return self.monitors[index];
}
- (void)setBrightness:(NSControl *)sender {
Monitor *m = [self getMonitorFrom:sender];
if (!m)
return;
eizo_set_brightness(m.monitor, sender.doubleValue);
}
- (void)setInputPort:(NSControl *)sender {
Monitor *m = [self getMonitorFrom:sender];
NSUInteger input = sender.tag % 0x1000;
if (!m)
return;
eizo_set_input_port(m.monitor, input);
NSEventModifierFlags mods = [NSEvent modifierFlags];
if (mods & NSEventModifierFlagShift) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/pmset";
task.arguments = @[@"sleepnow"];
[task launch];
}
}
- (void)appendMonitor:(Monitor *)m toMenu:(NSMenu *)menu base:(NSInteger)base {
NSMenuItem *titleItem = [NSMenuItem new];
titleItem.attributedTitle = [[NSAttributedString alloc]
initWithString:[NSString stringWithFormat:@"%s %s",
m.monitor->product, m.monitor->serial]
attributes:@{ NSFontAttributeName: [NSFont boldSystemFontOfSize:0] }];
[menu addItem:titleItem];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Brightness"]];
double brightness = 0;
(void) eizo_get_brightness(m.monitor, &brightness);
// XXX: So, while having a slider is strictly more useful,
// this is not something you're supposed to do in AppKit, if only because:
// - It does not respond to keyboard.
// - Positioning it properly is dark magic.
NSSlider *slider = [NSSlider
sliderWithValue:brightness minValue:0. maxValue:1.
target:self action:@selector(setBrightness:)];
slider.tag = base;
slider.continuous = true;
NSView *sliderView = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 200., slider.knobThickness + 2.)];
[sliderView addSubview:slider];
slider.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint activateConstraints:@[
[slider.leftAnchor
constraintEqualToAnchor:sliderView.leftAnchor constant:+23.],
[slider.rightAnchor
constraintEqualToAnchor:sliderView.rightAnchor constant:-6.],
[slider.centerYAnchor
constraintEqualToAnchor:sliderView.centerYAnchor]
]];
NSMenuItem *brightnessItem = [[NSMenuItem alloc]
initWithTitle:@"" action:nil keyEquivalent:@""];
brightnessItem.view = sliderView;
[menu addItem:brightnessItem];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Input ports"]];
uint16_t ports[16] = {0}, current = 0;
eizo_get_input_ports(m.monitor, ports, sizeof ports / sizeof ports[0] - 1);
(void) eizo_get_input_port(m.monitor, &current);
if (!ports[0])
ports[0] = current;
// USB-C ports are a bit tricky, they only need to be /displayed/ as such.
struct eizo_profile_item *item =
&m.monitor->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS];
for (size_t i = 0; ports[i]; i++) {
uint8_t usb_c = 0;
for (size_t u = 0; u < item->len / 2; u++)
if (ports[i] == peek_u16le(item->data + u * 2))
usb_c = u + 1;
NSString *title = nil;
if (!usb_c)
title = [NSString stringWithUTF8String:eizo_port_to_name(ports[i])];
else if (usb_c == 1)
title = [NSString stringWithUTF8String:g_port_names_usb_c[0]];
else
title = [NSString stringWithFormat:@"%s %u",
g_port_names_usb_c[0], usb_c];
NSMenuItem *inputPortItem = [[NSMenuItem alloc]
initWithTitle:title action:@selector(setInputPort:)
keyEquivalent:@""];
inputPortItem.tag = base + ports[i];
if (ports[i] == current)
inputPortItem.state = NSControlStateValueOn;
[menu addItem:inputPortItem];
}
}
- (void)showMenu {
struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0);
NSMutableArray<Monitor *> *monitors = [NSMutableArray array];
NSMenu *menu = [NSMenu new];
[menu setDelegate:self];
for (struct hid_device_info *p = devs; p; p = p->next) {
struct eizo_monitor *m = calloc(1, sizeof *m);
if (!m)
continue;
if (!eizo_monitor_open(m, p)) {
message_error("%s", m->error);
free(m);
continue;
}
Monitor *monitor = [[Monitor alloc] initWithMonitor:m];
[self appendMonitor:monitor toMenu:menu base:0x1000 * monitors.count];
[menu addItem:[NSMenuItem separatorItem]];
[monitors addObject:monitor];
}
if (!monitors.count) {
NSMenuItem *item = [[NSMenuItem alloc]
initWithTitle:@"No monitors found" action:nil keyEquivalent:@""];
item.enabled = false;
[menu addItem:item];
}
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:[[NSMenuItem alloc]
initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"]];
self.monitors = monitors;
// XXX: Unfortunately, this is not how menus should behave,
// but we really want to generate the menu on demand.
self.statusItem.menu = menu;
[self.statusItem.button performClick:nil];
self.statusItem.menu = nil;
}
- (void)menuDidClose:(NSMenu *)menu {
// Close and free up the devices as soon as possible, but no sooner.
dispatch_async(dispatch_get_main_queue(), ^{
self.monitors = nil;
});
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSStatusBar *systemBar = [NSStatusBar systemStatusBar];
self.statusItem = [systemBar statusItemWithLength:NSSquareStatusItemLength];
if (!self.statusItem.button)
return;
// Not bothering with templates,
// the icon would need to have a hole through it to look better.
NSImage *image = [NSApp applicationIconImage];
// One would expect the status bar to pick a reasonable size
// automatically, but that is not what happens.
image.size = NSMakeSize(systemBar.thickness, systemBar.thickness);
self.statusItem.button.image = image;
self.statusItem.button.action = @selector(showMenu);
}
@end
int
main(int argc, char *argv[])
{
@autoreleasepool {
if (argc > 1)
return run(argc, argv, message_output, message_error, true);
NSApplication *app = [NSApplication sharedApplication];
ApplicationDelegate *delegate = [ApplicationDelegate new];
app.delegate = delegate;
[app setActivationPolicy:NSApplicationActivationPolicyAccessory];
[app run];
}
return 0;
}
#endif