fixup() (install_name_tool \
-change @executable_path/../Resources/Libs/$1 @rpath/$1 $2)
fixup libmptag.dylib libemc.dylib
fixup libHIDmctrl.dylib libemc.dylib
fixup libcolour.dylib libemc.dylib
fixup libmctrl.dylib libHIDmctrl.dylib
This document is a collection of cleaned up personal notes. Feel free to contact me if you’re interested in continuing the efforts.
To be able to change the brightness and signal input of an EIZO ColorEdge display simply and quickly from all of Linux/Windows/macOS, but primarily Linux, which is my daily driver OS. Note that EIZO monitors are exceptional in that they do not support DDC/CI, for which I already have a solution.
I haven’t paid this much money for gatekeeping, and changing those settings on CG2700S has become even more annoying than on a CS2730.
What’s particularly insulting is that EIZO does provide such software for their cheap FlexScan series (called Screen InStyle), and that it could be rather easily adjusted to do ColorEdge as well, with an unimportant caveat.
✓ Reuse official libemc libraries, which covers all target platforms. While relatively easy to achieve, this doesn’t really integrate into my environment.
I’m leaving this as an exercise for the reader, you should have enough information.
✓ Create a fully open source, multiplatform tool to change the brightness and the current input of connected EIZO monitors. The result has a command line mode, and on Windows/macOS, also a systray utility.
✓ Create a filter tool for macOS/Linux tshark JSON output, in order to figure out what EIZO software is doing over the wires.
Windows, macOS, Linux computers
Self-destroyed EIZO CS2730, new EIZO CG2700S bought on sale
I have found the successful approaches too late, read on for further explanations:
EIZO-Brightness-Control interfaces with libemc, using interfaces decompiled from Screen InStyle.
myEIZOSensor contains an odd alternative libemc called libEizMonCtrl, with a corresponding header from somewhere. The author has forgotten where he got it from. Due to stripped symbols, it is otherwise not useful.
hid-monitor-control is somewhat hackish and limited, but actually reimplements the right thing.
hideizo and the related libeizo are a jackpot full of forbidden knowledge, however I can only use them as information resources, due to some of the author’s choices.
It took me way too much time to find out that my goals are actually technically possible at all. I had been a suffering customer for naught.
ColorNavigator 6 can calibrate an EIZO display, but is overall fairly limited in what else it can do.
ColorNavigator 7 can, in addition, read out and change the brightness and channel gain.
EIZO applications are actually primarily user interfaces to an internal library called libemc, short for EIZO Monitor Control. That includes Screen InStyle and Quick Color Match, where the macOS versions just need some install_name_tool adjustments.
As it turns out, libemc and the underlying libmctrl can control almost anything in both FlexScan and ColorEdge monitors.
The applications, and by extension libemc, can only be downloaded off the Internet for macOS and Windows. However, ColorNavigator 7 can also be obtained for Red Hat Linux by explicitly asking your local EIZO representative for it, as you’re instructed in its user manual. These Linux versions are missing from the online index. Portability-wise, the included libemc has very few dependencies, the most important of which is libusb 0.1; the rest is fairly generic for Linux C++ binaries.
We can reuse and inspect libemc further. Sadly, whether you reuse it, or reimplement it, you need to resort to some reverse engineering. There are no real header files to be found.
ColorNavigator locks the monitor, so that you can’t interrupt its operations.
ColorNavigator appears to use a hackish method to load and store profile settings, as it visibly switches through them, and it takes time.
ColorNavigator 6 is a Flash application, and can be decompiled with jpegxs-decompiler.
There is not too much to learn from it.
The monitor capability database is encrypted with the key in CNMonitorCapabilityData.as, see documentation; I’ve already decrypted its modern version.
ColorNavigator 7 is a QtWebEngine application, and has prettifyable Javascript sources.
On Windows, the actual sources are in \ProgramData\EIZO, not Program Files.
On macOS, the actual sources are in /Library/Application Support/EIZO, not /Applications.
find . -name '*.js' -exec uglifyjs --beautify --output {}.full.js — {} \;
There is not too much to learn from the Javascript, however html/scripts/lib/salt.js names integer constants applicable to libemc (for clean reverse engineering).
It uses REST APIs internally for things like the brightness setting, however this is separate from the external API. It points to sublibraries. In any case, it helps with reverse engineering.
It also includes some encrypted data, AES ECB/CBC encrypted,
with keys from cn7::crypto::EmbededKeyWithName()
.
Color InStyle is a .Net application, and the top level executables decompile well with usual .Net decompilers (for clean reverse engineering).
It was the most convenient for me to primarily use a MacBook throughout, however the decompiling tools do not care what you load in them.
Linux x86_64: seems to decompile the best
macOS x86_64: also decompiles well
macOS arm64: decompiles poorly, but this is what you’ll run on modern macOS
Windows x86 & x86_64: stripped symbols, still theoretically useful
The Screen InStyle .Net executable perhaps provides the cleanest information on the use of libemc, to the extent that it actually uses it. Note that some of the functions are missing from macOS/Linux libemc. The application hardcodes some monitor data on its own, e.g., FlexScan ports. (The ports list for most EIZO monitors is also hardcoded into the libemc library, though I haven’t figured out how to access it through its API.)
EIZO installers can be unpacked on Linux/macOS with innoextract—unzip and 7za don’t work, although there are clearly some ZIP entries within.
macOS packages can be unpacked without installing them by using bsdtar recursively, just mind that the paths below don’t strictly apply.
ColorNavigator 7’s bundled reset tool is a good minimal example of how libemc should be used, and it comes with its own library bundle. The library versions here might be older, though.
This is a logging library loaded on runtime that might provide a few runtime hints, but it seems like a hassle to get working.
ColorNavigator 7’s own various libraries decompile very well, and they generally show how individual libemc functions are supposed to be used.
The cn7::monitor
namespace is seemingly statically linked to all of them.
com.eizo.cn7.core/mac/libcnCore.dylib is about as good as any other.
The "bagel" namespace seems interesting.
bagel::emerror_string()
is an odd one,
though it pertains directly to libemc rather than to the wire protocol.
Screen InStyle is a bit more involved for reuse:
fixup() (install_name_tool \
-change @executable_path/../Resources/Libs/$1 @rpath/$1 $2)
fixup libmptag.dylib libemc.dylib
fixup libHIDmctrl.dylib libemc.dylib
fixup libcolour.dylib libemc.dylib
fixup libmctrl.dylib libHIDmctrl.dylib
Quick Color Match is similar, libraries need patching up.
Firmware updaters include an interesting library that names more HID usages, which aren’t actually even used by libemc. This is of interest for implementing custom interfaces.
Firmware files are encrypted using Blowfish (.efw) or AES-CBC (.efw2), and it is not made straight-forward to figure out. But you still can.
Ask your local EIZO support for a Linux package of ColorNavigator 7, then locate and extract the libraries. For reuse, they need some fixing up:
for i in lib*.so.?.*.*; do mv "$i" "${i%.*.*}"; done
for i in lib*.so.?; do ln -s "$i" "${i%.?}"; done
patchelf --set-rpath '$ORIGIN' lib*.so.?
EIZOHIDMonitor is a concrete class for an interface, and it’s all an abstract mess from there.
The encrypted monitor capability database is not particularly interesting, it contains candela values and some feature flags:
…/com.eizo.cn7.capability/MonitorCapability
…/com.eizo.cn7.core/mac/libcn7Core.dylib:
cn7::CapabilityCache::loadCapabilityFile
plusaes is this library. You can set a breakpoint on it to extract all keys.
The algorithm used is AES-128 CBC (some stuff can also be ECB):
openssl aes-128-cbc -d -in MonitorCapability -K 31d7f8280c1b0319c241131fee62402b -iv 7dc27f06199827f338be1c0e98b567c4
openssl aes-128-cbc -d -in SensorCapability -K c0a6d9a0ca597811e52d4d8924464f21 -iv ccb62a8b56e0c2e06820bd6968e4de60
The Linux build seems to be more obfuscated than macOS, however the keys were the same. Why bother.
EIZOMonitorProfile::GetAdditionalData()
hardcodes available ports
as EIZOInputPort
constructors for some but not all models
(my CS2730 is notably missing from there, but it provides profile data 0x53,
which is again missing in the newer CG2700S). USB-C inputs simply report
as DisplayPort (USB-C Alt Mode), but they may be identified by
monitor profile data 0x61.
Refer to ConvertPortValueToEmcIP()
to get the external values you can get
the names of, but it is roughly D-Sub, DVI, DisplayPort, HDMI, SDI, …
times 0x100, plus index.
This reconstructed listing might not be accurate:
CG243W | DVI1, DVI2, DP1
CG223W | DVI1, DVI2, DP1
CG245W | DVI1, DVI2, DP1
CG275W | DVI1, DP1, DP2
SX2462W | DVI1, DVI2, DP1
SX2262W | DVI1, DVI2, DP1
SX2762W | DVI1, DP1, DP2
S2232W | DVI1, DSUB1
S2432W | DVI1, DSUB1
S2242W | DVI1, DSUB1
S2233W | DP1, DVI1, DSUB1
S2433W | DP1, DVI1, DSUB1
S2243W | DP1, DVI1, DSUB1
EV2333W | DP1, DVI1, DSUB1
EV2334W | HDMI1, DVI1, DSUB1
EV2436W | DP1, DVI1, DSUB1
EV3237 | DVI1, DP1, DP2, HDMI1
EV2450 | DSUB1, DVI1, DP1, HDMI1
EV2455 | DSUB1, DVI1, DP1, HDMI1
EV2750 | DVI1, DP1, HDMI1
EV2451 | DSUB1, DVI1, DP1, HDMI1
EV2456 | DSUB1, DVI1, DP1, HDMI1
EV2457 | DVI1, DP1, HDMI1
EV2480 | DP1, DP2, HDMI1
EV2485 | DP1, DP2, HDMI1
EV2490 | DP1, DP2, HDMI1
EV2495 | DP1, DP2, HDMI1
EV2780 | DP1, DP2, HDMI1
EV2795 | DP1, DP2, HDMI1
EV3895 | DP1, DP2, HDMI1, HDMI2
CG3145 | DP1, DP2, HDMI1, HDMI2
CG3146 | SDI1, SDI2, SDI3, SDI4, SDIDUAL12, SDIDUAL34, SDIQUAD14, DP1, HDMI1
EV2785 | DP1, DP2, HDMI1, HDMI2
EV3285 | DP1, DP2, HDMI1, HDMI2
CG319X | DP1, DP2, HDMI1, HDMI2
CG279X | DVI1, DP1, DP2, HDMI1
CG2700X | DP1, DP2, HDMI1
CG2700S | DP1, DP2, HDMI1
CS2731 | DVI1, DP1, DP2, HDMI1
CS2410 | DVI1, DP1, HDMI1
CS2740 | DP1, DP2, HDMI1
EV2760 | DVI1, DP1, DP2, HDMI1
EV2360 | DSUB1, DP1, HDMI1
EV2460 | DSUB1, DVI1, DP1, HDMI1
EV2781 | DP1, DP2, HDMI1
EV2736W | DP1, DVI1
Use EmCtrlFind(NULL, 0)
with EmCtrlGetSerialProduct()
to initialize
the library and retrieve monitor ID pairs, then you can just try to use any
libemc function you have a prototype of. A Screen InStyle decompile should get
you far.
To cross-compile Windows utilities, you will need gendef to create a .a file for linking with MinGW:
MSYS2: pacman -S mingw-w64-x86_64-tools-git
Arch Linux: pacaur -S mingw-w64-tools
In Makefile terms:
%.def: %.dll
gendef - $< > $@
%.a: %.def
dlltool --input-def $< --output-lib $@
To make executables look at their own directory for the reused libraries:
macOS: install_name_tool -add_rpath @executable_path
Linux: -Wl,-rpath,'$ORIGIN'
Ghidra’s lldb integration cannot find Python’s psutil, and Homebrew’s pip3 doesn’t allow installing it.
The free version of Binary Ninja doesn’t support the arm64 architecture.
It seems like one has to trace functions with the debugger and Ghidra/BN side by side, at least if he doesn’t want to pay.
Qt Creator sucks massively with lldb.
Xcode is kind of fine: Debug - Debug Executable…
breakpoint set --func-regex ^Em[^p]
breakpoint set --func-regex ^CHIDDevice
It should be possible to generate debugging symbols and use any capable GDB UI, although I haven’t tried doing so. These projects are incidentally ELF-only: ghidra2dwarf, ghidra-ExportDwarfELFSymbols.
And you should be able to actually use both Ghidra’s and Binary Ninja’s debugger modes.
Wireshark’s USBHID filter/dissector (Analyze → Enabled Protocols…) sucks in that while it makes a few fields immediately readable, you also stop being able to select report data. With it disabled, remember that bRequests 1 and 9 are Get_Report and Set_Report respectively, and a wValue of 0x03XX is a Feature request concerning Report ID XX.
sudo ifconfig XHC{0,1,2} up
brew install --cask wireshark
Homebrew usbutils is close to pointless.
Homebrew usb.ids is sparse, and not used by lsusb either.
Wireshark can coïnstall USBPcap, which requires rebooting after installation.
Preferrably don’t toy around with the installation where you want to actually use ColorNavigator, such as by installing "USB Analyzer", and don’t install CN6 next to CN7. One of those actions made CN7 extremely laggy for me, and I don’t know how to fix it.
sudo modprobe usbmon
Either run the Windows version of ColorNavigator in VirtualBox, and pass it the monitor’s USB device, or run the Linux version.
The monitor kind-of follows USB HID, however it doesn’t use something standard like MCCS over USB, but rather a custom, evolving, complicated protocol. Report IDs are always included at the beginning of report data as usual.
- Initialization: EmCtrlFind
EIZOMonitor::FindAllMonitor -> EIZOMonitor::FindAllHIDMonitor
-> EmHIDFind -> EmFind -> EmFindEx
-> CHIDDevice::FindDevices
-> CHIDDevice::open -> CHHIDDeviceImplLeopard::open
-> CHHidDeviceImplLeopard::getCommandElements
-> CHIDDevice::payoutPINCode
-> CHIDDevice::getFixedData
-> CHIDDeviceImplLeopard::getFixedData
-> CHIDDevice::getFeatureValue
-> CHIDDeviceImplLeopard::getFeatureValue
- Read descriptor: bRequest 1, wValue 0x0306, wIndex 0, wLength 3
-> CHIDDevice::loadReportDescriptor
-> CHIDDevice::getFixedData
-> CHIDDeviceImplLeopard::getFixedData
-> CHIDDevice::setFeatureArray
-> CHIDDeviceImplLeopard::setFeatureArray
- Write request type: bRequest 9, wValue 0x0301, wIndex 0, wLength 517
-> CHIDDevice::getFeatureArray
-> CHIDDeviceImplLeopard::getFeatureArray
- Read response: bRequest 1, wValue 0x0301, wIndex 0, wLength 517
<repeats until the data is complete>
-> CHIDDeviceImplLeopard::setProperty
-> CHIDDevice::GetAllDevices
-> CountDevices
-> CHIDDevice{,ImplLeopard}::getProperty
-> CHIDDevice::GetProductGroup
-> EmHIDRetrieve -> EmRetrieve -> EmRetrieveEx
-> EmHIDGetFixedData -> EmGetFixedData -> EmGetFixedDataRaw
-> EmHIDGetData
-> EmGetFixedDataEx, EmGetFeatureArray
- Read serial product bRequest 1, wValue 0x0308, wIndex 0, wLength 25
-> EmCheckResult
EIZOMonitor:EmParseMonitorProfile
-> EIZOMonitorInterface::GetFixedDataMonitorProfile
-> EmHIDGetFixedData -> EmGetFixedData -> EmGetFixedDataRaw
-> EIZOMonitorInterface::GetMonitorprofile
-> EmHIDGetData
-> EmGetFixedDataEx, EmGetFeatureArray
- Read response: bRequest 1, wValue 0x0309, wIndex 0, wLength 112
-> EmCheckResult
-> EIZOMonitorInterface::Supported3DLUT
-> EmHIDGetFixedData -> EmGetFixedData
-> EIZOMonitorProfile::EIZOMonitorProfile
-> EmpTagParse, EmpGetTagDataSize, EmpGetTagData, Emp*...
EIZOMonitor::EmParseModeProfile
-> EmHIDGetFixedData -> EmGetFixedData
-> EmHIDGetData
-> EmGetFixedDataEx, EmGetFeatureArray, EmIsExclusiveMode
- Write request type: bRequest 9, wValue 0x0305, wIndex 0, wLength 529
- Read response: bRequest 1, wValue 0x0305, wIndex 0, wLength 529
-> EmCheckResult
- Read result: bRequest 1, wValue 0x0307, wIndex 0, wLength 8
-> EIZOModeProfile::EIZOModeProfile
-> EIZOModeProfile::parseProfileData -> Emp*...
- Read brightness: EmCtrlGetBrightnessForPercentage
EIZOMonitor::EmGetBrightness
-> EIZOMonitorInterface::GetBrightness
-> EIZOHIDMonitor::GetBrightness
-> EmHIDGetFixedData -> EmGetFixedData
-> EmHIDGetValue
-> EmGetFixedDataEx, EmGetFeatureValue
-> EmIsExclusiveMode
-> CHIDDeviceImplLeopard::setFeature*
- Write request type: bRequest 9, wValue 0x0303, wIndex 0, wLength 39
-> CHIDDeviceImplLeopard::getFeatureArray
- Read response: bRequest 1, wValue 0x0303, wIndex 0, wLength 39
-> EmCheckResult
- Read result: bRequest 1, wValue 0x0307, wIndex 0, wLength 8
- Write brightness: EmCtrlSetBrightnessForPercentage
- Write request type: bRequest 9, wValue 0x0302, wIndex 0, wLength 39
- Read result: bRequest 1, wValue 0x0307, wIndex 0, wLength 8
I stopped bothering to check code paths near the end, it’s actually fairly straight-forward once you realize how the USB interface is structured.
Linux makes looking at HID report descriptors the easiest, though this won’t get you the interesting one:
# uniq -c /sys/kernel/debug/hid/*:056D:*/rdesc
$ hexdump /sys/bus/hid/devices/*:056D:*/report_descriptor
Reports 1, 3, 5, 12, 14 use an odd scheme for retrieval where you first set Feature reports before you get them back with their values. Note that these reports all use the 0xff30 Usage Page:
The monitor adds a layer to the HID protocol, in that this Feature report returns another HID report descriptor that describes data contained within reports 2—5, 11—14.
Tha data start with two 16-bit fields denoting the starting offset, and the total size of data.
These two differ by report size. If it’s more than 32 bytes, use the larger report.
The data consist of: 16-bit HID Usage Page, 16-bit HID Usage ID, 16-bit PIN code, and finally the subreport, without its Report ID.
Analogical to setting features, but mind the set/get oddity.
This returns a linearly increasing 16-bit field that presumably serves to help resolve race conditions.
Used to check whether the last operation has been successful.
The data consist of: 16-bit HID Usage Page, 16-bit HID Usage ID, 16-bit PIN code, and an 8-bit result code that should not be negative.
The serial number is always 8 characters long, the rest of the data will be the model name, padded with spaces.
Key-value database of: 8-bit key, 8-bit length, data.
See the EIZOMonitorProfile
class.
This has been added in later models, such as the CG2700S.
The data is a 16-bit number, which you can both get and set. libemc sets it either to zero, or to its PIN code.
Like 2, 3, 4, 5, but for critical sections.
The data is additionally prepended by the 16-bit critical section number.
Button presses alone generate at least 4 Input reports, so the monitor is rather talkative. That said, not all possible changes are reported.
Presence of keys differs by monitor. For me, the important ones were:
0x53: a sequence of: 16-bit input port number, and some other 16-bit number
0x61: 16-bit USB-C input port numbers
The monitor maintains a "filesystem" where files have names in the scheme of ABC123. Filenames do not make a lot of sense. VER (firmware version?), EFW (more firmware information?), AIC (ASIC/FPGA data?), PRM (parameters?), EEP (EEPROM?), FLA (flash?), SYS (system?), INP (input?), LUT (look-up table?), MON (mode name?), INI (initial parameters?), BAK, BKL, MET, MOD, TMP…
"prg000" is used for firmware uploads.
I have captured a firmware update of my CS2730. This is the easiest way to decrypt at least parts of it—the .efw was over 2 MiB, the monitor received 1 MiB. There’s half of my plaintext!
$ tshark -r cs2730-fw-update.pcapng.gz -l -T ek --disable-protocol usbhid \
| NO_COLOR=1 go run eizo-pcap-decode.go \
| awk '$2 == "OUT" && $3 == "<>" && $4 == "ff0200fe" && $7 ~ /^prg000/ {
print substr($6, 21) }' \
| xxd -r -p > cs2730-fw-update.bin
The main microprocessor uses the Thumb-2 instruction set in little-endian. And there are symbols in the form of name references to the current function. It’s just not clear how to pair them automatically. Making sense of the code is still a difficult undertaking.
Linux’s generic HID driver takes over the USB device. It is preferrable to use hidapi-hidraw to communicate with that driver, rather than detaching it by using libusb. Either way, you will need to set up some evdev rules, or run as the superuser, to access the device.
On Windows and macOS, no special privileges are needed to communicate.
Keep in mind that the monitors accept firmware updates.
You have to parse both layers of HID report descriptors. Luckily you only need a very basic parser to retrieve the necessary data.
libemc is gnarly and full of special cases, so don’t aim unnecessarily high.
libeizo has the basics more or less right, and it names a ton of HID usages.
The rest of my attained know-how comes in the form of source code, see Goals.
EIZO is overpriced. I went for a second one because I wanted a mostly uniform display, and after the backlight of my former BenQ PD3220U failed just out of warranty, the second deciding factor was a decent warranty period.
My experiences say that with EIZO, you pay for properties rather than durability. LCDs suck.
Burnt panel after mere roughly 18 thousand hours, through single-person usage. It’s hard to take an accurate picture of what it looks like, as it appears oddly smooth, but suck it still does.
With a MacBook Pro M1 Pro connected over HDMI, after staying for too long in standby:
With a USB cable: the display decides to flash its power light orange and white, and refuse to cooperate until turned off and on through its power switch.
This is addressed by the 1.0002 firmware, which can’t be installed with new macOS.
Without a USB cable, or occasionally with new firmware: HDMI input goes haywire, showing static, until the display is turned off and on through the touch power button. Sometimes additional waiting and fiddling is necessary.
This seems like a possible MacBook Pro issue, maybe with HDCP, since one would expect encrypted signal to be noisy. It happens to a lot of people.
Two dark subpixels appeared after a few weeks, within the same pixel near the edge. This is the first time I’ve even seen this kind of defect on a computer monitor. And no, you can’t do much about it.
Comments
Use e-mail, webchat, or the form below. I'll also pick up on new HN, Lobsters, and Reddit posts.