aboutsummaryrefslogtreecommitdiff
path: root/eizoctl.c
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-11-25 02:52:59 +0100
committerPřemysl Eric Janouch <p@janouch.name>2024-11-25 02:52:59 +0100
commit15ea5b6a8e13d4d694ea1f256c37440f72f416bd (patch)
tree1ac2d580847dbf6ffdfb6d15a448dda2a3959b63 /eizoctl.c
downloadusb-drivers-15ea5b6a8e13d4d694ea1f256c37440f72f416bd.tar.gz
usb-drivers-15ea5b6a8e13d4d694ea1f256c37440f72f416bd.tar.xz
usb-drivers-15ea5b6a8e13d4d694ea1f256c37440f72f416bd.zip
Initial commit: eizoctl
Diffstat (limited to 'eizoctl.c')
-rw-r--r--eizoctl.c1433
1 files changed, 1433 insertions, 0 deletions
diff --git a/eizoctl.c b/eizoctl.c
new file mode 100644
index 0000000..6a335cb
--- /dev/null
+++ b/eizoctl.c
@@ -0,0 +1,1433 @@
+/*
+ * 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>
+
+#define PROJECT_NAME "eizoctl"
+
+#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 DEBUG
+ 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) {
+ eizo_monitor_failf(m, "unexpected HID usage");
+ return false;
+ }
+
+ // 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) {
+ eizo_monitor_failf(m, "failed to open: %ls", hid_error(NULL));
+ return false;
+ }
+
+ 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;
+ }
+
+ 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 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 void
+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) {
+ eizo_monitor_failf(m, "watch: %ls\n", hid_error(m->dev));
+ return;
+ }
+ 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 < 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'},
+ {}
+ };
+
+ 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;
+ 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))
+ 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)
+ eizo_watch(&m);
+ else
+ error("Watching events is not possible in this mode\n");
+ }
+
+ 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 -----------------------------------------------------------------
+#else
+
+#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};
+ struct eizo_profile_item *item = &m->profile[EIZO_PROFILE_KEY_INPUT_PORTS];
+ if (item->len) {
+ for (size_t i = 0; i < 15 && 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 < 15 && db && db[i]; i++)
+ ports[i] = db[i];
+ }
+
+ uint16_t current = 0;
+ (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.
+ 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) {
+ // FIXME: This seems to be too fast.
+ if (process_any_power_request())
+ eizo_set_input_port(m, id);
+ } 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(PROJECT_NAME),
+ };
+ if (!RegisterClassEx(&wc))
+ return 1;
+
+ // We need a window, but it can stay hidden.
+ g.hwnd = CreateWindowEx(WS_EX_CONTROLPARENT,
+ wc.lpszClassName, TEXT(PROJECT_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(PROJECT_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;
+}
+
+#endif