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:

  1. 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?
  2. 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”?
  3. 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?
  4. 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:

  1. Have software installed in all my target host computers that could coordinate sending the devices back and forth between then
  2. Hack my way through the RF protocol to inject commands to the devices
  3. 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!