Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mx-creative-console

Driving a Logitech MX Creative Keypad (046D:C354) from macOS over raw USB HID.

The Keypad's nine LCD keys are painted with images generated at runtime and its key presses read directly, with no Logitech software installed. The Dialpad is driven too, over Bluetooth: both rollers and all four buttons.

It runs as a Hammerspoon helper, so the two devices work as a second surface next to an Elgato Stream Deck, sharing the same hs.canvas rendering.

Build

make          # binaries land in bin/

Requires clang and the macOS SDK. No third-party dependencies.

Probes

Each binary does one thing.

bin/probe       # parse the HID report descriptor, list collections
bin/features    # enumerate the HID++ feature table
bin/cids        # enumerate 0x1B04 controls
bin/writetest   # confirm one handle accepts reports 0x11, 0x13 and 0x14
bin/claim       # keepalive and divert experiments
bin/listen      # print every report the device emits, never writes
bin/paint       # paint one LCD key
bin/demo        # paint all nine keys, then listen for presses
bin/dial        # Dialpad 0x4610 MultiRoller, read state or divert and listen
bin/display     # probe feature 0x00c3

features, cids and listen take a hex PID, so bin/features bc00 reads the Dialpad rather than the Keypad.

paint, demo and claim write to the device. Everything else is read-only.

The bridge

bin/helper is the long-lived process the Hammerspoon side talks to. It holds the keepalive, so the display and the input stream stay up for as long as it runs, and it speaks a line protocol.

commands  paint <key 0-8> <byteLen>\n      followed by byteLen raw JPEG bytes
          paintpng <key 0-8> <byteLen>\n   same, PNG, converted here
          paintfile <key 0-8> <path>\n     a PNG or JPEG on disk
          brightness <0-100>\n
          quit\n
events    ready keypad [dialpad]
          key <1-9>            LCD key down
          key up               all LCD keys released
          nav left|right       page arrow down
          nav up               page arrows released
          dial <0|1> <delta>   roller 1 the large dial, 0 the small wheel
          dialkey <1-4>        Dialpad button down
          dialkey up           Dialpad buttons released
          ping                 every 10 s, to a socket client only

The ping exists so a client can tell a dead socket from an idle one. The helper only speaks when a device is touched, so silence is otherwise meaningless.

Commands arrive on stdin or on a unix socket, /tmp/mx-creative-console.sock or $MXCC_SOCKET. Events go to stdout and to a connected socket client, and a client is sent the ready line when it connects.

Hammerspoon has to use the socket. hs.task:setInput only delivers data queued before start(), and silently drops everything written after, which is no use for a command stream that lives as long as the process.

paintpng exists because hs.canvas hands back PNG and Hammerspoon has no JPEG encoder. ImageIO is already linked for the paint path, so the conversion happens here rather than shelling out per repaint. paintfile keeps image bytes off the socket entirely, which hs.socket needs: it reports success on a large write and then drops the connection.

Running it

The helper holds the devices, so it wants to be a launch agent rather than a child of whatever is talking to it. com.shenshen.mx-creative-console runs it at login with KeepAlive, and the Hammerspoon module attaches to its socket.

launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.shenshen.mx-creative-console.plist
launchctl kickstart -k gui/$UID/com.shenshen.mx-creative-console   # restart after a rebuild

Add bin/helper to Input Monitoring in System Settings. The grant is per binary and launchd does not pass on a terminal's, so without it the Dialpad fails to open with kIOReturnNotPermitted (0xe00002e2) while the Keypad still works. Only the Dialpad needs it, because it presents as a mouse on Generic Desktop where the Keypad's vendor collection does not.

The Makefile signs with a self-signed identity, "MX Creative Console Helper", so the binary keeps one designated requirement across rebuilds and the grant sticks to the certificate rather than to a hash of the build. Signing is skipped when that identity is absent, so the build still works without it.

A linker-signed binary is ad-hoc, and TCC keys the grant to its cdhash, so every rebuild silently revoked it and the Dialpad quietly stopped opening. Creating the identity once:

openssl req -x509 -newkey rsa:2048 -keyout k.pem -out c.pem -days 3650 -nodes \
  -subj "/CN=MX Creative Console Helper" \
  -addext "basicConstraints=critical,CA:false" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=critical,codeSigning"
openssl pkcs12 -export -out c.p12 -inkey k.pem -in c.pem -passout pass:x \
  -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1
security import c.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -P x
security add-trusted-cert -r trustRoot -p codeSign -k ~/Library/Keychains/login.keychain-db c.pem

macOS rejects OpenSSL 3's default PKCS#12 encryption, hence the legacy algorithms, and wants digitalSignature in keyUsage as well as the codeSigning extended usage or the identity imports but will not sign.

Tunables, all optional:

Variable Default Purpose
MXCC_SOCKET /tmp/mx-creative-console.sock socket path
MXCC_KEEPALIVE_INTERVAL 1.0 seconds; 3.0 loses the display
MXCC_REFRESH_INTERVAL 4.0, agent sets 0 repaint timer, 0 to disable
MXCC_BRIGHTNESS_INTERVAL 0 re-assert timer, 0 to set once

The Dialpad is optional, since it runs on batteries. The ready line says which devices were found.

Painting one key from the shell:

sips -s format jpeg -z 118 118 icon.png --out key.jpg
{ printf 'paint 4 %d\n' "$(stat -f%z key.jpg)"; cat key.jpg; sleep 20; } | bin/helper

Reference

docs/PROTOCOL.md holds the wire format, the feature table, the LCD frame layout and what is verified versus untested. It also records two corrections to the published reverse engineering, on the meaning of byte 3 and on the geometry fields.

docs/DECISIONS.md compares the three ways to drive the device and records which one this repo takes and why.

docs/OPTIONS-PLUS.md documents the vendor stack, the Actions SDK limits, the on-disk profile store and how Options+ encodes keyboard chords.

Scope

Both devices work. The Keypad is USB; the Dialpad is Bluetooth, which makes it its own HID endpoint addressed at device index 0xFF, so the receiver-index problem never arises.

Devices are enumerated once at startup, so a Dialpad asleep at that moment stays invisible until the helper restarts. A hot-plug callback would fix it.

About

Driving a Logitech MX Creative Keypad and Dialpad from macOS over raw USB HID and Bluetooth HID++, with no Logitech software installed

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages