Compare commits

..

14 Commits

Author SHA1 Message Date
d8b01cdaee
eizoctl: add an option to filter by serial number
Some checks failed
Alpine 3.21 Scripts failed
It turns out that this is actually critical when switching inputs
in certain wild setups.
2025-07-31 21:16:16 +02:00
135f336a7c
eizoctl: add an option to list monitors 2025-07-31 21:15:26 +02:00
b4c1817c10
Update README.adoc
All checks were successful
Alpine 3.20 Success
2025-01-22 13:36:34 +01:00
32ea934947
Update README.adoc
All checks were successful
Alpine 3.20 Success
2025-01-22 13:25:39 +01:00
55984bc7ef
Bump copyright years
All checks were successful
Alpine 3.20 Success
2025-01-18 20:32:02 +01:00
dab190e857
eizoctl: fix message formatting on Windows
This increases the binary size, but at least we stop showing
Chinese characters instead of ASCII.
2025-01-18 20:32:02 +01:00
2594f8467d
eizoctl: also report USB-C from --input '?'
And related cleanup.
2025-01-18 20:32:02 +01:00
9039db44f6
eizoctl: make --events work with GUIs
Also add a --quiet option, and expand the program help.
2025-01-18 20:32:01 +01:00
89cab6fa39
eizoctl: display non-first numbers correctly
Humans count from 1, not from 0.
2025-01-18 18:57:16 +01:00
aea9c334e0
Bump liberty, replace help2man with help2adoc
All checks were successful
Alpine 3.20 Success
2025-01-01 06:09:49 +01:00
e53cddb030
Fix up code style adjustments 2024-12-26 12:26:19 +01:00
8832ba2227
Improve Windows packaging
All checks were successful
macOS Success
Alpine 3.20 Success
2024-12-23 17:14:59 +01:00
7bd6993b59
Bump liberty
All checks were successful
Alpine 3.20 Success
2024-12-17 06:38:47 +01:00
8717f425f4
eizo-pcap-decode.go: update for newer models
All checks were successful
Alpine 3.20 Success
2024-11-28 13:29:55 +01:00
9 changed files with 301 additions and 164 deletions

View File

@ -34,49 +34,7 @@ endif ()
# Dependencies # Dependencies
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (IconUtils)
# TODO(p): Shove this into IconUtils.cmake.
function (icon_to_iconset_size name svg size iconset outputs)
math (EXPR _size2x "${size} * 2")
set (_dimensions "${size}x${size}")
set (_png1x "${iconset}/icon_${_dimensions}.png")
set (_png2x "${iconset}/icon_${_dimensions}@2x.png")
set (${outputs} "${_png1x};${_png2x}" PARENT_SCOPE)
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (rsvg_convert_EXECUTABLE rsvg-convert ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${_png1x}" "${_png2x}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${iconset}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png1x}"
"--width=${size}" "--height=${size}" -- "${svg}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png2x}"
"--width=${_size2x}" "--height=${_size2x}" -- "${svg}"
DEPENDS "${svg}"
COMMENT "Generating ${name} ${_dimensions} icons" VERBATIM)
endfunction ()
function (icon_to_icns svg output_basename output)
get_filename_component (_name "${output_basename}" NAME_WE)
set (_iconset "${PROJECT_BINARY_DIR}/${_name}.iconset")
set (_icon "${PROJECT_BINARY_DIR}/${output_basename}")
set (${output} "${_icon}" PARENT_SCOPE)
set (_icon_png_list)
foreach (_icon_size 16 32 128 256 512)
icon_to_iconset_size ("${_name}" "${svg}"
"${_icon_size}" "${_iconset}" _icon_pngs)
list (APPEND _icon_png_list ${_icon_pngs})
endforeach ()
add_custom_command (OUTPUT "${_icon}"
COMMAND iconutil -c icns -o "${_icon}" "${_iconset}"
DEPENDS ${_icon_png_list}
COMMENT "Generating ${_name} icon" VERBATIM)
set_source_files_properties ("${_icon}" PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
endfunction ()
find_package (PkgConfig REQUIRED) find_package (PkgConfig REQUIRED)
pkg_check_modules (libusb libusb-1.0) pkg_check_modules (libusb libusb-1.0)
@ -138,7 +96,6 @@ endif ()
if (WITH_HIDAPI AND WIN32) if (WITH_HIDAPI AND WIN32)
list (APPEND targets_gui eizoctltray) list (APPEND targets_gui eizoctltray)
include (IconUtils)
set (icon_png_list) set (icon_png_list)
foreach (icon_size 16 32 48) foreach (icon_size 16 32 48)
icon_to_png (eizoctltray ${PROJECT_SOURCE_DIR}/eizoctltray.svg icon_to_png (eizoctltray ${PROJECT_SOURCE_DIR}/eizoctltray.svg
@ -181,25 +138,34 @@ endif ()
# Generate documentation from help output # Generate documentation from help output
if (NOT WIN32 AND NOT CMAKE_CROSSCOMPILING) if (NOT WIN32 AND NOT CMAKE_CROSSCOMPILING)
find_program (HELP2MAN_EXECUTABLE help2man) set (HELP2ADOC "${PROJECT_SOURCE_DIR}/liberty/tools/help2adoc.awk")
if (NOT HELP2MAN_EXECUTABLE) set (ASCIIMAN "${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk")
message (FATAL_ERROR "help2man not found")
endif ()
foreach (target ${targets}) foreach (target ${targets})
set (page_output "${PROJECT_BINARY_DIR}/${target}.1") set (page_adoc "${PROJECT_BINARY_DIR}/${target}.1.adoc")
list (APPEND project_MAN_PAGES "${page_output}") set (page_roff "${PROJECT_BINARY_DIR}/${target}.1")
add_custom_command (OUTPUT ${page_output} list (APPEND project_MAN_PAGES "${page_roff}")
COMMAND ${HELP2MAN_EXECUTABLE} -N
"${PROJECT_BINARY_DIR}/${target}" -o ${page_output} # $<TARGET_FILE:tgt> could be used, if we didn't have to escape it.
DEPENDS ${target} string (REPLACE "\\" "\\\\"
COMMENT "Generating man page for ${target}" VERBATIM) target_path "${PROJECT_BINARY_DIR}/${target}")
add_custom_command (OUTPUT "${page_adoc}"
COMMAND env LC_ALL=C awk -f "${HELP2ADOC}"
-v "Target=${target_path}" > "${page_adoc}"
DEPENDS "${target}" "${HELP2ADOC}"
COMMENT "Generating AsciiDoc man page for ${target}" VERBATIM)
add_custom_command (OUTPUT "${page_roff}"
COMMAND env LC_ALL=C awk -f "${ASCIIMAN}"
"${page_adoc}" > "${page_roff}"
DEPENDS "${page_adoc}" "${ASCIIMAN}"
COMMENT "Generating roff man page for ${target}" VERBATIM)
endforeach () endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
endif () endif ()
# The files to be installed # The files to be installed
if (NOT WIN32)
include (GNUInstallDirs) include (GNUInstallDirs)
# These should be accessible by users, but need to touch system devices. # These should be accessible by users, but need to touch system devices.
@ -213,13 +179,17 @@ install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR}
${SETUID}) ${SETUID})
install (TARGETS ${targets_gui} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS ${targets_gui} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
foreach (page ${project_MAN_PAGES}) foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}") string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}" install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach () endforeach ()
set (CPACK_SET_DESTDIR TRUE)
else ()
install (TARGETS ${targets} ${targets_gui} DESTINATION .)
endif ()
# CPack # CPack
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
@ -232,5 +202,4 @@ set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack) include (CPack)

View File

@ -1,4 +1,4 @@
Copyright (c) 2013, 2024, Přemysl Eric Janouch <p@janouch.name> Copyright (c) 2013, 2024 - 2025, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

7
NEWS
View File

@ -1,3 +1,10 @@
Unreleased
* eizoctl: added a --quiet option to suppress information and/or errors
* eizoctl: fixed input port reporting
1.1.0 (2024-11-28) 1.1.0 (2024-11-28)
* Ported eizoctltray to macOS as well * Ported eizoctltray to macOS as well

View File

@ -1,6 +1,7 @@
USB drivers USB drivers
=========== ===========
:compact-option: :compact-option:
:source-highlighter: chroma
_usb-drivers_ is a collection of utilities to control various hardware over USB. _usb-drivers_ is a collection of utilities to control various hardware over USB.
@ -26,6 +27,60 @@ it will also suspend or power off the system, respectively.
image:eizoctltray-win.png["eizoctltray on Windows with expanded menu", 343, 278] image:eizoctltray-win.png["eizoctltray on Windows with expanded menu", 343, 278]
image:eizoctltray-mac.png["eizoctltray on macOS with expanded menu", 343, 278] image:eizoctltray-mac.png["eizoctltray on macOS with expanded menu", 343, 278]
Installation
^^^^^^^^^^^^
On Windows, copy it to
__Users\*\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup__.
On macOS, copy it to the _Applications_ folder,
then add it in _System Settings → General → Login Items → Open at Login_.
Automation
^^^^^^^^^^
_eizoctltray_ can also be used the same way as _eizoctl_, just with any output
redirected to message windows, rather than a console window or a terminal.
This is useful for automation, such as with AutoHotkey.
Beware that Windows is not a fan of how rapidly EIZO monitors switch upstream
USB ports. Thus, if you invoke port switching with keyboard shortcuts,
remember to add a small delay, so that pressed modifier keys are not remembered.
You will also want to silence any error messages.
.AutoHotkey example
```autohotkey
#Requires AutoHotkey v2.0
exe := A_Startup . "\eizoctltray.exe"
^#F1:: { ; Windows + Control + F1
Sleep 500
Run exe " -qq --input HDMI"
}
^#F2:: { ; Windows + Control + F2
Sleep 500
Run exe " -qq --input DP"
}
^#F3:: { ; Windows + Control + F3
Sleep 500
Run exe " -qq --input USB-C"
}
#Home:: { ; Windows + Home
Run exe " -q --brightness +0.1"
}
#End:: { ; Windows + End
Run exe " -q --brightness -0.1"
}
```
On macOS, the simplest way to bind keyboard shortcuts is the Shortcuts app,
with _Run Shell Scripts_ actions:
```
/Applications/eizoctltray.app/Contents/MacOS/eizoctltray -q --input HDMI
```
If you have issues with entering a specific key combination, like I did
with ^⌘F1 etc., try changing it later within _System Settings_ → _Keyboard_ →
_Keyboard Shortcuts..._ → _Services_ → _Shortcuts_.
elksmart-comm elksmart-comm
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
_elksmart-comm_ interfaces with ELK Smart infrared dongles EKX4S and EKX5S-T, _elksmart-comm_ interfaces with ELK Smart infrared dongles EKX4S and EKX5S-T,
@ -53,13 +108,13 @@ Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version You can get a package with the latest development version
as a https://git.janouch.name/p/nixexprs[Nix derivation]. as a https://git.janouch.name/p/nixexprs[Nix derivation].
Windows binaries can be downloaded from Windows/macOS binaries can be downloaded from
https://git.janouch.name/p/usb-drivers/releases[the Releases page on Gitea]. https://git.janouch.name/p/usb-drivers/releases[the Releases page on Gitea].
Building Building
-------- --------
Build dependencies: Build dependencies:
CMake, pkg-config, liberty (included), help2man + CMake, pkg-config, liberty (included) +
Runtime dependencies: Runtime dependencies:
libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi >= 0.14 (eizoctl) libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi >= 0.14 (eizoctl)

View File

@ -73,10 +73,17 @@ var (
fmtIn, fmtOut, fmtReset string fmtIn, fmtOut, fmtReset string
) )
func decodeSubreport(data []byte) string { func decodeSubreport(id byte, data []byte) string {
if len(data) < 6 { critical := isCriticalSubreport(id)
if len(data) < 6 || critical && len(data) < 8 {
return fmt.Sprintf("%x", data) return fmt.Sprintf("%x", data)
} }
var cs uint16
if critical {
data, cs = data[2:], le.Uint16(data[0:2])
}
usage := uint32(le.Uint16(data[:2]))<<16 | uint32(le.Uint16(data[2:4])) usage := uint32(le.Uint16(data[:2]))<<16 | uint32(le.Uint16(data[2:4]))
filtered := make([]byte, len(data)-6) filtered := make([]byte, len(data)-6)
for i, b := range data[6:] { for i, b := range data[6:] {
@ -86,8 +93,17 @@ func decodeSubreport(data []byte) string {
filtered[i] = b filtered[i] = b
} }
} }
return fmt.Sprintf("<> %08x %04x %x %s", usage, le.Uint16(data[4:6]), if critical {
data[6:], string(filtered)) return fmt.Sprintf("<> %08x %04x=%04x %x %s",
usage, cs, le.Uint16(data[4:6]), data[6:], string(filtered))
} else if usage == 0xff0000f1 {
// No idea what this is, but it follows the format.
return fmt.Sprintf("<> %08x %04x %s",
usage, le.Uint16(data[4:6]), decodeMP(data[6:]))
} else {
return fmt.Sprintf("<> %08x %04x %x %s",
usage, le.Uint16(data[4:6]), data[6:], string(filtered))
}
} }
func decodeResult(data []byte) string { func decodeResult(data []byte) string {
@ -131,6 +147,14 @@ func isGetSubreport(id byte) bool {
return false return false
} }
func isCriticalSubreport(id byte) bool {
switch id {
case 11, 12, 13, 14:
return true
}
return false
}
func isSubreport(id byte) bool { func isSubreport(id byte) bool {
return isSetSubreport(id) || isGetSubreport(id) return isSetSubreport(id) || isGetSubreport(id)
} }
@ -143,7 +167,7 @@ func processInterrupt(p *Packet) {
if *raw { if *raw {
fmt.Printf("%s INT %02x %x\n", p.addr(), data[0], data[1:]) fmt.Printf("%s INT %02x %x\n", p.addr(), data[0], data[1:])
} else if isSubreport(data[0]) { } else if isSubreport(data[0]) {
fmt.Printf("%s INT %s\n", p.addr(), decodeSubreport(data[1:])) fmt.Printf("%s INT %s\n", p.addr(), decodeSubreport(data[0], data[1:]))
} }
} }
@ -163,7 +187,7 @@ func processControl(p *Packet) {
fmt.Printf("%s IN SR %x\n", p.addr(), data[5:]) fmt.Printf("%s IN SR %x\n", p.addr(), data[5:])
} else if isGetSubreport(data[0]) { } else if isGetSubreport(data[0]) {
fmt.Printf("%s IN %s%s%s\n", p.addr(), fmt.Printf("%s IN %s%s%s\n", p.addr(),
fmtIn, decodeSubreport(data[1:]), fmtReset) fmtIn, decodeSubreport(data[0], data[1:]), fmtReset)
} else if data[0] == 6 { } else if data[0] == 6 {
fmt.Printf("%s IN PC %04x\n", p.addr(), le.Uint16(data[1:])) fmt.Printf("%s IN PC %04x\n", p.addr(), le.Uint16(data[1:]))
} else if data[0] == 7 { } else if data[0] == 7 {
@ -172,6 +196,8 @@ func processControl(p *Packet) {
fmt.Printf("%s IN ID %s %s\n", p.addr(), data[1:9], data[9:]) fmt.Printf("%s IN ID %s %s\n", p.addr(), data[1:9], data[9:])
} else if data[0] == 9 { } else if data[0] == 9 {
fmt.Printf("%s IN MP %s\n", p.addr(), decodeMP(data[1:])) fmt.Printf("%s IN MP %s\n", p.addr(), decodeMP(data[1:]))
} else if data[0] == 10 {
fmt.Printf("%s IN CS %04x\n", p.addr(), le.Uint16(data[1:]))
} else { } else {
fmt.Printf("%s IN %02x %x\n", p.addr(), data[0], data[1:]) fmt.Printf("%s IN %02x %x\n", p.addr(), data[0], data[1:])
} }
@ -180,7 +206,9 @@ func processControl(p *Packet) {
fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:]) fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:])
} else if isSetSubreport(data[0]) { } else if isSetSubreport(data[0]) {
fmt.Printf("%s OUT %s%s%s\n", p.addr(), fmt.Printf("%s OUT %s%s%s\n", p.addr(),
fmtOut, decodeSubreport(data[1:]), fmtReset) fmtOut, decodeSubreport(data[0], data[1:]), fmtReset)
} else if data[0] == 10 {
fmt.Printf("%s OUT CS %04x\n", p.addr(), le.Uint16(data[1:]))
} else if data[0] != 1 && !isGetSubreport(data[0]) { } else if data[0] != 1 && !isGetSubreport(data[0]) {
fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:]) fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:])
} }

216
eizoctl.c
View File

@ -4,7 +4,7 @@
* This program stays independent of the liberty library * This program stays independent of the liberty library
* in order to build on Windows. * in order to build on Windows.
* *
* Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name> * Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@ -18,6 +18,14 @@
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
* *
*/ */
// On Windows, vswprintf() interprets %s in the width of the format string,
// and %hs is not really compliant with any standard:
// https://devblogs.microsoft.com/oldnewthing/20190830-00/?p=102823
#ifdef _WIN32
#define __USE_MINGW_ANSI_STDIO
#endif
#include <stdarg.h> #include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@ -41,7 +49,9 @@
#define hid_init hidapi_hid_init #define hid_init hidapi_hid_init
#endif #endif
#if defined __GNUC__ #if defined __MINGW_GNU_PRINTF
#define ATTRIBUTE_PRINTF(x, y) __MINGW_GNU_PRINTF((x), (y))
#elif defined __GNUC__
#define ATTRIBUTE_PRINTF(x, y) __attribute__((format(printf, x, y))) #define ATTRIBUTE_PRINTF(x, y) __attribute__((format(printf, x, y)))
#else #else
#define ATTRIBUTE_PRINTF(x, y) #define ATTRIBUTE_PRINTF(x, y)
@ -803,7 +813,7 @@ eizo_port_by_name(const char *name)
return index; return index;
} }
static char * static const char *
eizo_port_to_name(uint16_t port) eizo_port_to_name(uint16_t port)
{ {
const char *stem = NULL; const char *stem = NULL;
@ -811,14 +821,14 @@ eizo_port_to_name(uint16_t port)
if (group && group < sizeof g_port_names / sizeof g_port_names[0]) if (group && group < sizeof g_port_names / sizeof g_port_names[0])
stem = g_port_names[group][0]; stem = g_port_names[group][0];
static char buffer[32] = ""; static char buf[32] = "";
if (!stem) if (!stem)
snprintf(buffer, sizeof buffer, "%x", port); snprintf(buf, sizeof buf, "%x", port);
else if (!number) else if (!number)
snprintf(buffer, sizeof buffer, "%s", stem); snprintf(buf, sizeof buf, "%s", stem);
else else
snprintf(buffer, sizeof buffer, "%s %d", stem, number); snprintf(buf, sizeof buf, "%s %d", stem, (number + 1));
return buffer; return buf;
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -895,7 +905,7 @@ eizo_get_input_ports(struct eizo_monitor *m, uint16_t *ports, size_t size)
} }
static uint16_t static uint16_t
eizo_resolve_port(struct eizo_monitor *m, const char *port) eizo_resolve_port_by_name(struct eizo_monitor *m, const char *port)
{ {
uint8_t usb_c_index = 0; uint8_t usb_c_index = 0;
if (eizo_port_by_name_in_group(port, g_port_names_usb_c, &usb_c_index)) { if (eizo_port_by_name_in_group(port, g_port_names_usb_c, &usb_c_index)) {
@ -907,6 +917,26 @@ eizo_resolve_port(struct eizo_monitor *m, const char *port)
return eizo_port_by_name(port); return eizo_port_by_name(port);
} }
static const char *
eizo_resolve_port_to_name(struct eizo_monitor *m, uint16_t port)
{
// 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 (uint8_t i = 0; i < item->len / 2; i++) {
if (port != peek_u16le(item->data + i * 2))
continue;
static char buf[32] = "";
if (!i)
snprintf(buf, sizeof buf, "%s", g_port_names_usb_c[0]);
else
snprintf(buf, sizeof buf, "%s %u", g_port_names_usb_c[0], (i + 1));
return buf;
}
return eizo_port_to_name(port);
}
static bool static bool
eizo_set_input_port(struct eizo_monitor *m, uint16_t port) eizo_set_input_port(struct eizo_monitor *m, uint16_t port)
{ {
@ -934,8 +964,40 @@ eizo_restart(struct eizo_monitor *m)
// --- Main -------------------------------------------------------------------- // --- Main --------------------------------------------------------------------
struct catbuf {
char buf[4096];
size_t len;
};
static const char *
catf(struct catbuf *b, const char *format, ...) ATTRIBUTE_PRINTF(2, 3);
static const char *
catf(struct catbuf *b, const char *format, ...)
{
va_list ap;
va_start(ap, format);
int result = vsnprintf(b->buf + b->len, sizeof b->buf - b->len, format, ap);
va_end(ap);
if (result >= 0) {
b->len += result;
if (b->len >= sizeof b->buf)
b->len = sizeof b->buf - 1;
}
return b->buf;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef void (*print_fn)(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
static void print_dummy(const char *format, ...)
{
(void) format;
}
static bool static bool
eizo_watch(struct eizo_monitor *m) eizo_watch(struct eizo_monitor *m, print_fn output, print_fn error)
{ {
uint8_t buf[1024] = {}; uint8_t buf[1024] = {};
int res = 0; int res = 0;
@ -945,58 +1007,85 @@ eizo_watch(struct eizo_monitor *m)
if (buf[0] != EIZO_REPORT_ID_GET && if (buf[0] != EIZO_REPORT_ID_GET &&
buf[0] != EIZO_REPORT_ID_GET_LONG) { buf[0] != EIZO_REPORT_ID_GET_LONG) {
printf("Unknown report ID\n"); error("Unknown report ID: %02x\n", buf[0]);
continue; continue;
} }
struct catbuf message = {{0}, 0};
uint16_t page = peek_u16le(&buf[1]), id = peek_u16le(&buf[3]); uint16_t page = peek_u16le(&buf[1]), id = peek_u16le(&buf[3]);
uint32_t usage = page << 16 | id; uint32_t usage = page << 16 | id;
printf("%08x", usage); catf(&message, "%08x", usage);
const struct parser_report *r = eizo_monitor_subreport(m, usage); const struct parser_report *r = eizo_monitor_subreport(m, usage);
if (!r) { if (!r) {
printf(" unknown usage\n"); output(catf(&message, " unknown usage\n"));
continue; continue;
} }
size_t rlen = r->report_size / 8 * r->report_count; size_t rlen = r->report_size / 8 * r->report_count;
if ((size_t) res < 7 + rlen) { if ((size_t) res < 7 + rlen) {
printf(" received data too short\n"); output(catf(&message, " received data too short\n"));
continue; continue;
} }
if (r->report_size == 16) if (r->report_size == 16)
for (size_t i = 0; i + 1 < rlen; i += 2) for (size_t i = 0; i + 1 < rlen; i += 2)
printf(" %04x", peek_u16le(&buf[7 + i])); catf(&message, " %04x", peek_u16le(&buf[7 + i]));
else else
for (size_t i = 0; i < rlen; i++) for (size_t i = 0; i < rlen; i++)
printf(" %02x", buf[7 + i]); catf(&message, " %02x", buf[7 + i]);
printf("\n"); output(catf(&message, "\n"));
} }
} }
typedef void (*print_fn)(const char *format, ...) ATTRIBUTE_PRINTF(1, 2); static const char *usage = "Usage: %s OPTION...\n\n"
" -l, --list\n"
" List all connected EIZO monitors, with their serial number.\n"
" -s, --serial SERIAL\n"
" Only act on the monitor matching the specified serial number.\n"
" -b, --brightness [+-]BRIGHTNESS\n"
" Change monitor brightness; values go from 0 to 1 and may be relative.\n"
" -i, --input NAME\n"
" Change monitor input ports; use '?' to retrieve current values.\n"
" -r, --restart\n"
" Reboot monitors.\n"
" -e, --events\n"
" Watch for events reported by monitors.\n"
" -q, --quiet\n"
" Use once to suppress informative messages, twice to suppress errors.\n"
" -h, --help\n"
" Display this help and exit.\n"
" -V, --version\n"
" Output version information and exit.\n";
static int static int
run(int argc, char *argv[], print_fn output, print_fn error, bool verbose) run(int argc, char *argv[], print_fn output, print_fn error)
{ {
const char *name = argv[0]; const char *name = argv[0];
const char *usage = "Usage: %s [--brightness [+-]BRIGHTNESS] [--input NAME]"
" [--restart] [--events]\n";
static struct option opts[] = { static struct option opts[] = {
{"input", required_argument, NULL, 'i'}, {"list", no_argument, NULL, 'l'},
{"serial", required_argument, NULL, 's'},
{"brightness", required_argument, NULL, 'b'}, {"brightness", required_argument, NULL, 'b'},
{"input", required_argument, NULL, 'i'},
{"restart", no_argument, NULL, 'r'}, {"restart", no_argument, NULL, 'r'},
{"events", no_argument, NULL, 'e'}, {"events", no_argument, NULL, 'e'},
{"quiet", no_argument, NULL, 'q'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'}, {"version", no_argument, NULL, 'V'},
{} {}
}; };
int quiet = 0;
double brightness = NAN; double brightness = NAN;
bool relative = false, restart = false, events = false; bool list = false, relative = false, restart = false, events = false;
const char *port = NULL; const char *serial = NULL, *port = NULL;
int c = 0; int c = 0;
while ((c = getopt_long(argc, argv, "b:i:h", opts, NULL)) != -1) while ((c = getopt_long(argc, argv, "ls:b:i:reqhV", opts, NULL)) != -1)
switch (c) { switch (c) {
case 'l':
list = true;
break;
case 's':
serial = optarg;
break;
case 'b': case 'b':
relative = *optarg == '+' || *optarg == '-'; relative = *optarg == '+' || *optarg == '-';
if (sscanf(optarg, "%lf", &brightness) && isfinite(brightness)) if (sscanf(optarg, "%lf", &brightness) && isfinite(brightness))
@ -1013,6 +1102,9 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
case 'e': case 'e':
events = true; events = true;
break; break;
case 'q':
quiet++;
break;
case 'h': case 'h':
output(usage, name); output(usage, name);
return 0; return 0;
@ -1038,9 +1130,11 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
error("%ls\n", hid_error(NULL)); error("%ls\n", hid_error(NULL));
return 1; return 1;
} }
if (quiet > 0)
output = print_dummy;
if (quiet > 1)
error = print_dummy;
// 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; struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0), *p = devs;
for (; p; p = p->next) { for (; p; p = p->next) {
struct eizo_monitor m = {}; struct eizo_monitor m = {};
@ -1049,6 +1143,15 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
continue; continue;
} }
if (list)
output("%s %s\n", m.product, m.serial);
// Generously assuming that different products/models
// don't share serial numbers,
// which would otherwise deserve another filtering option.
if (serial && strcmp(serial, m.serial))
goto next;
if (isfinite(brightness)) { if (isfinite(brightness)) {
double prev = 0.; double prev = 0.;
if (!eizo_get_brightness(&m, &prev)) { if (!eizo_get_brightness(&m, &prev)) {
@ -1057,25 +1160,25 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
double next = relative ? brightness + prev : brightness; double next = relative ? brightness + prev : brightness;
if (!eizo_set_brightness(&m, next)) if (!eizo_set_brightness(&m, next))
error("Failed to set brightness: %s\n", m.error); error("Failed to set brightness: %s\n", m.error);
else if (verbose) else
output("%s %s: brightness: %.2f -> %.2f\n", output("%s %s: brightness: %.2f -> %.2f\n",
m.product, m.serial, prev, next); m.product, m.serial, prev, next);
} }
} }
if (port) { if (port) {
uint16_t prev = 0; uint16_t prev = 0;
uint16_t next = eizo_resolve_port(&m, port); uint16_t next = eizo_resolve_port_by_name(&m, port);
if (!eizo_get_input_port(&m, &prev)) { if (!eizo_get_input_port(&m, &prev)) {
error("Failed to get input port: %s\n", m.error); error("Failed to get input port: %s\n", m.error);
} else if (!strcmp(port, "?")) { } else if (!strcmp(port, "?")) {
output("%s %s: input: %s\n", output("%s %s: input: %s\n",
m.product, m.serial, eizo_port_to_name(prev)); m.product, m.serial, eizo_resolve_port_to_name(&m, prev));
} else if (!next) { } else if (!next) {
error("Failed to resolve port name: %s\n", port); error("Failed to resolve port name: %s\n", port);
} else { } else {
if (!eizo_set_input_port(&m, next)) if (!eizo_set_input_port(&m, next))
error("Failed to set input port: %s\n", m.error); error("Failed to set input port: %s\n", m.error);
else if (verbose) else
output("%s %s: input: %s -> %s\n", output("%s %s: input: %s -> %s\n",
m.product, m.serial, eizo_port_to_name(prev), port); m.product, m.serial, eizo_port_to_name(prev), port);
} }
@ -1083,16 +1186,17 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
if (restart) { if (restart) {
if (!eizo_restart(&m)) if (!eizo_restart(&m))
error("Failed to restart: %s\n", m.error); error("Failed to restart: %s\n", m.error);
else if (verbose) else
output("%s %s: restart\n", m.product, m.serial); output("%s %s: restart\n", m.product, m.serial);
} }
if (events) { if (events) {
if (!verbose) if (quiet)
error("Watching events is not possible in this mode\n"); error("Watching events is not possible in this mode\n");
else if (!eizo_watch(&m)) else if (!eizo_watch(&m, output, error))
error("%s\n", m.error); error("%s\n", m.error);
} }
next:
eizo_monitor_close(&m); eizo_monitor_close(&m);
} }
hid_free_enumeration(devs); hid_free_enumeration(devs);
@ -1124,7 +1228,7 @@ stdio_error(const char *format, ...)
int int
main(int argc, char *argv[]) main(int argc, char *argv[])
{ {
return run(argc, argv, stdio_output, stdio_error, true); return run(argc, argv, stdio_output, stdio_error);
} }
// --- Windows ----------------------------------------------------------------- // --- Windows -----------------------------------------------------------------
@ -1167,8 +1271,8 @@ message_output(const char *format, ...)
wchar_t *message = message_printf(format, ap); wchar_t *message = message_printf(format, ap);
va_end(ap); va_end(ap);
if (message) { if (message) {
MessageBox( MessageBox(NULL, message,
NULL, message, NULL, MB_ICONINFORMATION | MB_OK | MB_APPLMODAL); L"Message", MB_ICONINFORMATION | MB_OK | MB_APPLMODAL);
free(message); free(message);
} }
} }
@ -1228,21 +1332,9 @@ append_monitor(struct eizo_monitor *m, HMENU menu, UINT_PTR base)
if (!ports[0]) if (!ports[0])
ports[0] = current; 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++) { for (size_t i = 0; ports[i]; i++) {
uint8_t usb_c = 0; snwprintf(buf, sizeof buf, L"%s",
for (size_t u = 0; u < item->len / 2; u++) eizo_resolve_port_to_name(m, ports[i]));
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; UINT flags = MF_STRING;
if (ports[i] == current) if (ports[i] == current)
@ -1395,7 +1487,7 @@ wWinMain(
char *mb = mbargv[i + 1] = calloc(len, sizeof *mb); char *mb = mbargv[i + 1] = calloc(len, sizeof *mb);
wcstombs(mb, argv[i], len); wcstombs(mb, argv[i], len);
} }
return run(argc + 1, mbargv, message_output, message_error, false); return run(argc + 1, mbargv, message_output, message_error);
} }
LocalFree(argv); LocalFree(argv);
@ -1609,23 +1701,9 @@ message_error(const char *format, ...)
if (!ports[0]) if (!ports[0])
ports[0] = current; 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++) { for (size_t i = 0; ports[i]; i++) {
uint8_t usb_c = 0; NSString *title = [NSString stringWithUTF8String:
for (size_t u = 0; u < item->len / 2; u++) eizo_resolve_port_to_name(m.monitor, ports[i])];
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] NSMenuItem *inputPortItem = [[NSMenuItem alloc]
initWithTitle:title action:@selector(setInputPort:) initWithTitle:title action:@selector(setInputPort:)
@ -1708,7 +1786,7 @@ main(int argc, char *argv[])
{ {
@autoreleasepool { @autoreleasepool {
if (argc > 1) if (argc > 1)
return run(argc, argv, message_output, message_error, true); return run(argc, argv, message_output, message_error);
NSApplication *app = [NSApplication sharedApplication]; NSApplication *app = [NSApplication sharedApplication];
ApplicationDelegate *delegate = [ApplicationDelegate new]; ApplicationDelegate *delegate = [ApplicationDelegate new];

@ -1 +1 @@
Subproject commit 492815c8fc38ad6e333b2f1c5094a329e3076155 Subproject commit 9268fb8eba4a60499809965b3b69c2eb7e3798e7