Introduction
Do you ever wonder: what could I take as my next project? Well, I do. And it usually begins with the thought of acquiring a new device for tinkering.
But what if the thingy costs as much as a kidney? Or if it will take 3 weeks to get to your place?
The epiphany I had, and which I wanted to share, is the fact that there is joy in just messing around with stuff that you have at hand. Especially with the things that you use every day and in with the right point of view, can review a lot of secret goodies.
This thought occurred to me while staring at my current desk setup at my new home office. In front of me I have a standing desk that I bought during work from home COVID era, a Logitech Bluetooth/wireless keyboard and mouse set, my PC (nothing fancy), a Bluetooth soundbar that I use as my PC speaker, a USB-C docking station that I use to plug/charge my work laptop in the occasional time I bring it home, my monitor and an external webcam.
This setup is very functional, making it easy to work with my Linux PC or switching to my work Windows laptop in less than 10 sec. It fits my taste of not being cluttered and has the right tones of grey and matte black everywhere.
However, setups like these always have room for improvement. As you may already have anticipated, the first idea that I got was to improve on the integration of the stuff I have laying around and to fix some aesthetics hiccups of my setup.
flowchart TD PC["PC"] Monitor["Monitor"] Keyboard["Keyboard"] Mouse["Mouse"] Soundbar["Soundbar"] DockingStation["Docking Station"] Webcam["Webcam"] Laptop["Laptop"] PC -->|DisplayPort| Monitor PC -.->|Bluetooth| Soundbar PC -.->|"Logitech Wireless"| Keyboard PC -.->|"Logitech Wireless"| Mouse PC -->|"USB-B"| Monitor DockingStation -->|"USB-C"| Monitor Monitor -->|"USB-A"| Webcam Laptop -->|"USB-C PD"| DockingStation Laptop -.->|"Logitech Wireless"| Keyboard Laptop -.->|"Logitech Wireless"| Mouse
Listing some ideas down here, without any particular order:
- Switching back and forth between my PC and work laptop though fast, still requires 3 steps: switching my keyboard, my mouse and finally the monitor. Can I make them all work in sync?
- I have to turn on my soundbar every time I start using my PC after more than 30 min. Is there a way to get “wake on Bluetooth”?
- My standing desk has a digital panel, connected through a RJ45 cable to the motor controller. Why can’t it be controlled by my PC?
- I find it kind of ugly having to leave a USB-C cable hanging out of the front of my docking station to charge my work laptop. How could I still get my laptop charged without this loose cable?
This will be a multi-part series of posts on how I get myself lost in those superfluous, but interesting (for me at least) topics.
Seamless KVM
My ideal workflow would be to use my Logi MX Keys host switching keys (it capability to be connected up to 3 devices - same as my Logi MX Master 3 mouse) to switch all my connections (monitor, keyboard, mouse) at once.
This would be possible if the keyboard has a way to inform to the host device which one is currently being assigned and if this host device could command the monitor and mouse to switch to the corresponding device.
This should be especially tricky if the host device loses its connections to the keyboard when it is no longer the selected device. But let’s check what can be done.
My keyboard is currently connected to my PC through this proprietary Logi wireless protocol to communicate with the USB dongle plugged in my PC.
XXXXXXXXX@ws1:~$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 001 Device 002: ID 8087:0a2b Intel Corp. Bluetooth wireless interface
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
So, lsusb shows the dongle to be “Logitech, Inc. Unifying Receiver” (Wikipedia). Searching around in Google, it seems that the dongle uses an extended version of the HID protocol (“HID++”) and there are already some user friendly API implementation on Linux for receiving and transmitting commands through it. First that I’ve found was Solaar, which, after unwisely creating udev rules to allow my user to have write access to /dev/hidraw0, I could list the capabilities of the receiver and my keyboard and mouse:
XXXXXXXXX@ws1:~$ solaar show
Solaar version 1.1.1
Unifying Receiver
Device path : /dev/hidraw0
USB id : 046d:C52B
Serial : XXXXXXXXX
Firmware : 12.11.B0032
Bootloader : 04.16
Other : AA.AA
Has 2 paired device(s) out of a maximum of 6.
Notifications: wireless, software present (0x000900)
Device activity counters: 1=75, 2=22
1: MX Keys Keyboard
Device path : /dev/hidraw1
WPID : 408A
Codename : MX Keys
Kind : keyboard
Protocol : HID++ 4.5
Polling rate : 20 ms (50Hz)
Serial number: XXXXXXXXX
Model ID: B35B408A0000
Unit ID: XXXXXXXXX
Bootloader: BL1 08.00.B0011
Firmware: MPK 12.01.B0013
Other:
The power switch is located on the edge of top right corner.
Supports 33 HID++ 2.0 features:
0: ROOT {0000}
1: FEATURE SET {0001}
2: DEVICE FW VERSION {0003}
Firmware: Bootloader BL1 08.00.B0011 00008169E8BB
Firmware: Firmware MPK 12.01.B0013 408AFE037737
Firmware: Other
Unit ID: XXXXXXXXX Model ID: B35B408A0000 Transport IDs: {'btleid': 'B35B', 'wpid': '408A'}
3: DEVICE NAME {0005}
Name: MX Keys Wireless Keyboard
Kind: keyboard
4: WIRELESS DEVICE STATUS {1D4B}
5: RESET {0020}
6: DEVICE FRIENDLY NAME {0007}
Friendly Name: MX Keys
7: BATTERY STATUS {1000}
Battery: 50%, discharging, next level 20%.
8: REPROG CONTROLS V4 {1B04}
Key/Button Diversion (saved): {'10': 0, '110': 0, '111': 0, '191': 0, '199': 0, '200': 0, '209': 0, '210': 0, '211': 0, '224': 0, '225': 0, '226': 0, '227': 0, '228': 0, '229': 0, '230': 0, '231': 0, '232': 0, '233': 0, '234': 0, '235': 0, '236': 0}
Key/Button Diversion : {'209': 0, '210': 0, '211': 0, '199': 0, '200': 0, '224': 0, '225': 0, '110': 0, '226': 0, '227': 0, '228': 0, '229': 0, '230': 0, '231': 0, '232': 0, '233': 0, '10': 0, '191': 0, '234': 0, '111': 0, '236': 0, '235': 0}
9: CHANGE HOST {1814}
Change Host : 1:XXXXXXXXX
10: HOSTS INFO {1815}
Host 0 (paired): XXXXXXXXX
Host 1 (paired): XXXXXXXXX
Host 2 (paired): XXXXXXXXX
11: BACKLIGHT2 {1982}
Backlight (saved): True
Backlight : True
12: K375S FN INVERSION {40A3}
Swap Fx function (saved): False
Swap Fx function : False
13: ENCRYPTION {4100}
14: LOCK KEY STATE {4220}
15: KEYBOARD DISABLE KEYS {4521}
Disable keys (saved): {'1': False, '16': False, '2': False, '4': False, '8': False}
Disable keys : {'1': False, '2': False, '4': False, '8': False, '16': False}
16: MULTIPLATFORM {4531}
Set OS (saved): 0
Set OS : Windows
17: DFUCONTROL SIGNED {00C2}
18: DEVICE RESET {1802} internal, hidden
19: unknown:1803 {1803} internal, hidden
20: CONFIG DEVICE PROPS {1806} internal, hidden
21: unknown:1813 {1813} internal, hidden
22: OOBSTATE {1805} internal, hidden
23: unknown:1830 {1830} internal, hidden
24: unknown:1890 {1890} internal, hidden
25: unknown:1891 {1891} internal, hidden
26: unknown:18A1 {18A1} internal, hidden
27: unknown:1DF3 {1DF3} internal, hidden
28: unknown:1E00 {1E00} hidden
29: unknown:1EB0 {1EB0} internal, hidden
30: unknown:1861 {1861} internal, hidden
31: unknown:1A20 {1A20} internal, hidden
32: unknown:18B0 {18B0} internal, hidden
Has 24 reprogrammable keys:
0: Host Switch Channel 1 , default: HostSwitch Channel 1 => HostSwitch Channel 1
nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
1: Host Switch Channel 2 , default: HostSwitch Channel 2 => HostSwitch Channel 2
nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
2: Host Switch Channel 3 , default: HostSwitch Channel 3 => HostSwitch Channel 3
nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
3: Brightness Down , default: Brightness Down => Brightness Down
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:1, group:0, group mask:empty
reporting: default
4: Brightness Up , default: Brightness Up => Brightness Up
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:2, group:0, group mask:empty
reporting: default
5: Mission Control/Task View , default: Mission Control/Task View => Mission Control/Task View
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:3, group:0, group mask:empty
reporting: default
6: Dashboard Launchpad/Action Center, default: Dashboard Launchpad/Action Center => Dashboard Launchpad/Action Center
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:4, group:0, group mask:empty
reporting: default
7: Show Desktop , default: Show Desktop => Show Desktop
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:5, group:0, group mask:empty
reporting: default
8: Backlight Down , default: Backlight Down => Backlight Down
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:6, group:0, group mask:empty
reporting: default
9: Backlight Up , default: Backlight Up => Backlight Up
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:7, group:0, group mask:empty
reporting: default
10: Previous Fn , default: Previous => Previous
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:8, group:0, group mask:empty
reporting: default
11: Play/Pause Fn , default: Play/Pause => Play/Pause
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:9, group:0, group mask:empty
reporting: default
12: Next Fn , default: Next => Next
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:10, group:0, group mask:empty
reporting: default
13: Mute Fn , default: Mute => Mute
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:11, group:0, group mask:empty
reporting: default
14: Volume Down Fn , default: Volume Down => Volume Down
is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:12, group:0, group mask:empty
reporting: default
15: Volume Up Fn , default: Volume Up => Volume Up
nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
16: Calculator , default: Calculator => Calculator
nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
17: Screen Capture/Print Screen, default: Screen Capture => Screen Capture
nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
18: App Contextual Menu/Right Click, default: Right Click/App Contextual Menu => Right Click/App Contextual Menu
nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
19: Lock PC , default: WindowsLock => WindowsLock
nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
20: Left Arrow , default: Keyboard Left Arrow => Keyboard Left Arrow
nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
21: Right Arrow , default: Keyboard Right Arrow => Keyboard Right Arrow
nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
22: F Lock , default: Do Nothing One => Do Nothing One
is FN, analytics key events, pos:0, group:0, group mask:empty
reporting: default
23: unknown:0034 , default: Do Nothing One => Do Nothing One
nonstandard, analytics key events, pos:0, group:0, group mask:empty
reporting: default
Battery: 50%, discharging, next level 20%.
2: MX Master 3 Wireless Mouse
Device path : /dev/hidraw2
WPID : 4082
Codename : MX Master 3
Kind : mouse
Protocol : HID++ 4.5
Polling rate : 8 ms (125Hz)
Serial number: XXXXXXXXX
The power switch is located on the base.
Battery: unknown (device is offline).
So, with a simple solaar config 2 change-host 3, I could switch my mouse to the host configured on button 3. This is great as I was planning to have my mouse following my keyboard’s events. However, when I tried to switch the mouse back to my PC:
XXXXXXXXX@ws1:~$ solaar config 2 change-host 3
Setting change-host of MX Master 3 Wireless Mouse to 3
rodrigo@ws1:~$ solaar config 2 change-host 1
solaar: error: Traceback (most recent call last):
File "/usr/share/solaar/lib/solaar/cli/__init__.py", line 204, in run
m.run(c, args, _find_receiver, _find_device)
File "/usr/share/solaar/lib/solaar/cli/config.py", line 124, in run
raise Exception("no online device found matching '%s'" % device_name)
Exception: no online device found matching '2'
Oh-oh! I’ve lost connection to the mouse! In a way, it is obvious: why would you want a host that is no longer receiving inputs from the device to be able to affect what another host currently connected get? Kind of a security issue, don’t you think?
So, I can make the mouse go from my PC to work laptop, but I cannot bring it back. Let me think through my options:
- Have software installed in all my target host computers that could coordinate sending the devices back and forth between then
- Hack my way through the RF protocol to inject commands to the devices
- Give up
It is still too early for option 3. What about option 1? Let’s take a deeper look into it. Searching around in Logitech’s site, I’ve found this app they created called “Logitech Flow”. According to its whitepaper, it is almost exactly what I described in option 1, with some more extra mechanism to auto-discover hosts through the internet (sketchy) and some fancy UI to switch hosts when the mouse get to the edge of the monitor (reminded me of the old Synergy implementation).
And, what do you say, some cool folks already implemented an open source version of the tool in python, using the libraries that the team behind Solaar created: logitech-flow-kvm. I had to fight a bit with pip to get it installed (needed to have patchelf installed and a newer version of the Meson build system), but in the end it worked flawlessly.
However, at the end, I won’t go for it. It won’t work for me for 2 reasons: a) I don’t want to install any non-compliant software in my work laptop and risk policy breaches and b) my 3rd host (the one configure to button 3), is an old iPad pro that I use to play Minecraft Bedrock with my kids. I do not want to mess with coding for it right now.
Ok, so let’s get our hands dirty with the radio protocol. What RF protocol does my logitech devices use to communicate to the receiver? According to uberOptions wiki:
Logitech’s current wireless keyboards and mice run on one of four wireless protocols:
- The proprietary 27 MHz “FastRF” protocol
- A proprietary 2.4 GHz wireless protocol (mostly mice, very few keyboards), I’ll label this “2.4 GHz Old”
- The standard 2.4 GHz Bluetooth protocol
- A proprietary 2.4 GHz “Unifying” wireless protocol (not compatible with any of the above)
Fair to assume that we are under the last one there. I could go to the route using LOGITacker tool which more or less would allow such injection. But once more it won’t fly for me for 2 reasons: a) I don’t have one of these cool RF dev boards (Nordic nRF52840, MakerDiary MDK or April Brother dongles) laying around and buying one defeats the purpose of this project (although very very tempting); and b) I would probably need to flash my receivers to an old firmware that is still vulnerable to the attacks (not recommended, at least, but doable with the munifying tool).
Sooo, give up? Let’s park this temporarily while we explore something else - controlling my monitor. To be precise I have a Dell P3421W 34 in monitor, which has some very interesting properties.
It supports the DDC/CI (Display Data Channel Command Interface) protocol, meaning it implements MCCS (Monitor Control Command Set) over I2C. It basically allows one to send commands equivalent to anything that the on screen menu has.
On Linux, we can use the ddcutil tool (documentation). If we run a query to list the capabilities of the monitor:
XXXXXXXXX@ws1:~$ ddcutil capabilities
Model: P3421W
MCCS version: 2.1
Commands:
Op Code: 01 (VCP Request)
Op Code: 02 (VCP Response)
Op Code: 03 (VCP Set)
Op Code: 07 (Timing Request)
Op Code: 0C (Save Settings)
Op Code: E3 (Capabilities Reply)
Op Code: F3 (Capabilities Request)
VCP Features:
Feature: 02 (New control value)
Feature: 04 (Restore factory defaults)
Feature: 05 (Restore factory brightness/contrast defaults)
Feature: 08 (Restore color defaults)
Feature: 10 (Brightness)
Feature: 12 (Contrast)
Feature: 14 (Select color preset)
Values:
05: 6500 K
08: 9300 K
0b: User 1
0c: User 2
Feature: 16 (Video gain: Red)
Feature: 18 (Video gain: Green)
Feature: 1A (Video gain: Blue)
Feature: 52 (Active control)
Feature: 60 (Input Source)
Values:
1b: Unrecognized value
0f: DisplayPort-1
11: HDMI-1
Feature: AC (Horizontal frequency)
Feature: AE (Vertical frequency)
Feature: B2 (Flat panel sub-pixel layout)
Feature: B6 (Display technology type)
Feature: C6 (Application enable key)
Feature: C8 (Display controller type)
Feature: C9 (Display firmware level)
Feature: CC (OSD Language)
Values:
02: English
03: French
04: German
06: Japanese
09: Russian
0a: Spanish
0d: Chinese (simplified / Kantai)
0e: Portuguese (Brazil)
Feature: D6 (Power mode)
Values:
01: DPM: On, DPMS: Off
04: DPM: Off, DPMS: Off
05: Write only value to turn off display
Feature: DC (Display Mode)
Values:
00: Standard/Default mode
03: Movie
05: Games
Feature: DF (VCP Version)
Feature: E0 (Manufacturer specific feature)
Feature: E1 (Manufacturer specific feature)
Feature: E2 (Manufacturer specific feature)
Values: 00 1D 02 04 0E 12 14 (interpretation unavailable)
Feature: E5 (Manufacturer specific feature)
Feature: E7 (Manufacturer specific feature)
Values: 00 02 (interpretation unavailable)
Feature: E8 (Manufacturer specific feature)
Feature: E9 (Manufacturer specific feature)
Values: 00 01 02 21 22 24 (interpretation unavailable)
Feature: F0 (Manufacturer specific feature)
Values: 00 0C (interpretation unavailable)
Feature: F1 (Manufacturer specific feature)
Feature: F2 (Manufacturer specific feature)
Feature: FD (Manufacturer specific feature)
Ah-ha! We have Feature 60 (Input Source) ready to be used to toggle between DisplayPort (PC - 0x0F), HDMI (not currently used - 0x11) or USB-C (marked here as “Unrecognized value” - 0x1B).
So, with some glue shell script we could achieve a part of our goal mixing logitech-flow-kvm and ddcutil tools to control the monitor to follow the keyboard:
XXXXXXXXX@ws1:~$ logitech-flow-kvm watch --on-disconnect-execute="ddcutil setvcp 60 0x1B" --on-connect-execute="ddcutil setvcp 60 0x0F" /dev/hidraw0:1
Listening for connection events for /dev/hidraw0:1
Press CTRL+C to exit
❌ Device disconnected
Executed 'ddcutil setvcp 60 0x1B'; status 0.
✅ Device connected
Executed 'ddcutil setvcp 60 0x0F'; status 0.
And it works! It is an improvement compared to before (now it’s just keyboard and mouse that needs fiddling), but I am not overall satisfied.
What I didn’t share is another cool trick that this monitor has: it has an embedded USB KVM which allows the 4 downstream USB-A 3.0 ports to be shared to the USB-B or USB-C upstream ports. In the next post in this series I will explore if I can use it in a more seamless way than we achieved here.
Conclusion
While still not nearly over, this small investigation of the junk I have laying around in my desk already provided me a couple of hours of fun, learning and trying new things. Also, I was fortunate to stumble in a lot of potential paths to explore such as an RF hacking project or even the cool thing that my keyboard has a light sensor which could be used for home automation with Home Assistant. Let’s see what we get from this!