logiops icon indicating copy to clipboard operation
logiops copied to clipboard

Logitech Master 4 Settings - WORKING EXAMPLE

Open vjeko2404 opened this issue 3 months ago • 15 comments

MX Master 4 initial CID mapping and full gesture configuration Device released: 30.09.2025 CID map reverse-engineered manually – verified on Arch Linux Config below includes: – SmartShift toggle – Thumb rest gesture → media control (volume, track, play/pause) – Super/meta thumb key – Browser nav bindings

Button map reference included for maintainers.

//  Logitech MX Master 4 Button Mapping                             
//  0x0c4  → Top button behind scroll wheel (MagSpeed toggle)      
//  0x052  → Middle click (wheel press)  (Standard middle click)
//  0x053  → Back button (side) (Browser Back)  
//  0x056  → Forward button (side) (Browser Forward)   
//  0x0c3  → Gesture button (Gesture button) (Media gesture hub)   
//  0x1a0  → Thumb button (bottom-left corner) (Super/Meta key)     

//  Configuration for Logitech MX Master 4      
//  Full gesture implementation on the gesture button for media control 

devices: (
  {
    name: "MX Master 4";

    // Set the DPI.
    dpi: 4000;

    // Enable smartshift to automatically switch between ratchet and free-spin.
    smartshift: {
      on: true;
      threshold: 15;
    };

    // Enable high-resolution scrolling for a smoother feel.
    hiresscroll: {
      on: true;
    };

    buttons: (
      // ── Top button (behind scroll wheel) ── Toggles SmartShift
      {
        cid: 0xc4;
        action: {
          type: "ToggleSmartshift";
        };
      },

      // ── Back button (side) ──────────────── Browser Back
      {
        cid: 0x53;
        action: {
          type: "Keypress";
          keys: [ "KEY_BACK" ];
        };
      },

      // ── Forward button (side) ───────────── Browser Forward
      {
        cid: 0x56;
        action: {
          type: "Keypress";
          keys: [ "KEY_FORWARD" ];
        };
      },

      // ── Thumb rest click ────────────────── Super/Windows key
      {
        cid: 0x1a0;
        action: {
          type: "Keypress";
          keys: [ "KEY_LEFTMETA" ];
        };
      },

      // ── Gesture button ──────────────────── Media Gestures
      {
        cid: 0xc3;
        action: {
          type: "Gestures";
          gestures: (
            // Hold + Move Up ──────────────── Volume Up
            {
              direction: "Up";
              mode: "OnRelease";
              action: {
                type: "Keypress";
                keys: [ "KEY_VOLUMEUP" ];
              };
            },

            // Hold + Move Down ────────────── Volume Down
            {
              direction: "Down";
              mode: "OnRelease";
              action: {
                type: "Keypress";
                keys: [ "KEY_VOLUMEDOWN" ];
              };
            },

            // Hold + Move Left ────────────── Previous Track
            {
              direction: "Left";
              mode: "OnRelease";
              action: {
                type: "Keypress";
                keys: [ "KEY_PREVIOUSSONG" ];
              };
            },

            // Hold + Move Right ───────────── Next Track
            {
              direction: "Right";
              mode: "OnRelease";
              action: {
                type: "Keypress";
                keys: [ "KEY_NEXTSONG" ];
              };
            },

            // Simple click (no movement) ──── Play/Pause
            {
              direction: "None";
              mode: "OnRelease";
              action: {
                type: "Keypress";
                keys: [ "KEY_PLAYPAUSE" ];
              };
            }
          );
        };
      }
    );
  }
);

vjeko2404 avatar Oct 01 '25 19:10 vjeko2404

Nice! What's the status on haptic feedback? Does it work?

fstoltz avatar Oct 02 '25 06:10 fstoltz

Nice! What's the status on haptic feedback? Does it work?

The button itself works and it does vibrate when pressed, but that's about it. I didn't use the mice with "professional" software like CAD or whatever but I also seriously doubt, that haptic feedback can work in linux.

vjeko2404 avatar Oct 02 '25 06:10 vjeko2404

There is even one more dimension to care about, as beside the haptic feedback of the thumb button, the ratcheting force of the wheel is now also configurable. I'm looking forward to see this. As of now the loudest noise the mouse produces is the actual haptic feedback, this is an amazing upgrade.

wiebel avatar Oct 04 '25 14:10 wiebel

Is it possible to set Meta + left mouse click here?

// ── Thumb rest click ────────────────── Super/Windows key { cid: 0x1a0; action: { type: "Keypress"; keys: [ "KEY_LEFTMETA" ]; }; },

haorxor avatar Oct 06 '25 16:10 haorxor

@vjeko2404

I also seriously doubt, that haptic feedback can work in linux.

It would be nice to allow sending a haptic feedback event over IPC to logid. This way, other programs could be configured to vibrate the mouse. E.g., write a small script that listens for org.freedesktop.Notifications and vibrates the mouse if there is a new notification on screen. Or more practically, OnInterval gestures could vibrate the mouse every interval to provide feedback when adjusting thinks like volume and virtual desktops.

However, that would require adding support for hid++2.0 (?) haptic feedback. I guess that standard way to reverse engineer this (and the vibration / ratchet intensity settings) would be to run Logitech Options in a Windows virtual machine and capture the USB traffic with Wireshark?

kris7t avatar Oct 27 '25 15:10 kris7t

Few days further in I found that the ratcheting force is already available since mx3, just laking dokumentation, it's called torque:

    smartshift: {
      ...                                                                                                                                                                                                                                             
      torque: 100;                                                                                                                                                                                                                                            
    };  

The haptic feedback is already available and is not necessarily the scope of logiops. An Example can be found here: https://github.com/lukasfri/mx4notifications But as of now it only works via bolt not with bt.

wiebel avatar Oct 28 '25 05:10 wiebel

Unfortunately, https://github.com/lukasfri/mx4notifications/issues/1 It doesn't work for me either over bolt.

Arguably, handling the haptics should be best placed in logid, since it already has the hid++ device open...

Here are the hid++ features supported by the MX Master 4:

# hidpp-list-features /dev/hidraw13
MX Master 4 (046d:b042) is a HID++ 4.5 device
Feature 0x01: [0x0001] Feature set
Feature 0x02: [0x0003] Device FW version
Feature 0x03: [0x0005] Device name
Feature 0x04: [0x1d4b] Wireless device status
Feature 0x05: [0x0020] Reset
Feature 0x06: [0x0021] Crypto Identifier
Feature 0x07: [0x0007] Device Friendly Name
Feature 0x08: [0x0011] ?
Feature 0x09: [0x1004] ?
Feature 0x0a: [0x1701] ?
Feature 0x0b: [0x19b0] ?
Feature 0x0c: [0x19c0] ?
Feature 0x0d: [0x1b04] Reprog controls v4
Feature 0x0e: [0x1814] Change host
Feature 0x0f: [0x1815] Hosts info
Feature 0x10: [0x2250] ?
Feature 0x11: [0x2111] ?
Feature 0x12: [0x2121] Hi-res wheel
Feature 0x13: [0x2150] ?
Feature 0x14: [0x2201] Adjustable dpi
Feature 0x15: [0x2251] ?
Feature 0x16: [0x00d1] ?
Feature 0x17: [0x1802] Device reset (hidden, internal)
Feature 0x18: [0x1803] ? (hidden, internal)
Feature 0x19: [0x1807] ? (hidden, internal)
Feature 0x1a: [0x1816] ? (hidden, internal)
Feature 0x1b: [0x1805] OOBState (hidden, internal)
Feature 0x1c: [0x1830] ? (hidden, internal)
Feature 0x1d: [0x1891] ? (hidden, internal)
Feature 0x1e: [0x18a1] ? (hidden, internal)
Feature 0x1f: [0x1e00] Enable hidden features (hidden)
Feature 0x20: [0x1e02] ? (hidden, internal)
Feature 0x21: [0x1e22] ? (hidden, internal)
Feature 0x22: [0x1e30] ? (hidden, internal)
Feature 0x23: [0x1602] ?
Feature 0x24: [0x1eb0] ? (hidden, internal)
Feature 0x25: [0x1861] ? (hidden, internal)
Feature 0x26: [0x9205] ? (hidden, internal)
Feature 0x27: [0x9201] ? (hidden, internal)
Feature 0x28: [0x9300] ? (hidden, internal)
Feature 0x29: [0x9401] ? (hidden, internal)
Feature 0x2a: [0x9402] ? (hidden, internal)
Feature 0x2b: [0x9001] ? (hidden, internal)
Feature 0x2c: [0x18b1] ? (hidden, internal)
Feature 0x2d: [0x18c0] ? (hidden, internal)

In particular, 0x0B4E, which the mx4notifications repo tries to call in https://github.com/lukasfri/mx4notifications/blob/694928e5c6952acf61541d5bec2142e40be9c1e0/src/mx_master_4.py#L122 , doesn't appear here, so that code is likely bogus and vibe coded according to the commit messages.

Out of these, haptic feedback seems to to be Feature 0x0b: [0x19b0]:

  • Method 0x2 sets haptic feedback strength. The first parameter is always 0x01, the second is the feedback strength between 0-100: off (0), subtle (15), low (45), medium (60), high (100).
  • Method 0x4 produces a haptic feedback effect. The first parameter is the effect type.
    • Effect type 0 when turning feedback ON from OFF and also when switching virtual desktops with the gesture button. This is very prominent vibration with two clicks.
    • Effect type 4 when hovering on actions in the action ring. This is a single subtle click.
    • Effect type 8 triggered after changing effect strength to preview the strength. This is a sequence of vibrations with varying intensity.

kris7t avatar Oct 28 '25 12:10 kris7t

Unfortunately, lukasfri/mx4notifications#1 It doesn't work for me either over bolt.

Arguably, handling the haptics should be best placed in logid, since it already has the hid++ device open...

Here are the hid++ features supported by the MX Master 4:

# hidpp-list-features /dev/hidraw13
MX Master 4 (046d:b042) is a HID++ 4.5 device
Feature 0x01: [0x0001] Feature set
Feature 0x02: [0x0003] Device FW version
Feature 0x03: [0x0005] Device name
Feature 0x04: [0x1d4b] Wireless device status
Feature 0x05: [0x0020] Reset
Feature 0x06: [0x0021] Crypto Identifier
Feature 0x07: [0x0007] Device Friendly Name
Feature 0x08: [0x0011] ?
Feature 0x09: [0x1004] ?
Feature 0x0a: [0x1701] ?
Feature 0x0b: [0x19b0] ?
Feature 0x0c: [0x19c0] ?
Feature 0x0d: [0x1b04] Reprog controls v4
Feature 0x0e: [0x1814] Change host
Feature 0x0f: [0x1815] Hosts info
Feature 0x10: [0x2250] ?
Feature 0x11: [0x2111] ?
Feature 0x12: [0x2121] Hi-res wheel
Feature 0x13: [0x2150] ?
Feature 0x14: [0x2201] Adjustable dpi
Feature 0x15: [0x2251] ?
Feature 0x16: [0x00d1] ?
Feature 0x17: [0x1802] Device reset (hidden, internal)
Feature 0x18: [0x1803] ? (hidden, internal)
Feature 0x19: [0x1807] ? (hidden, internal)
Feature 0x1a: [0x1816] ? (hidden, internal)
Feature 0x1b: [0x1805] OOBState (hidden, internal)
Feature 0x1c: [0x1830] ? (hidden, internal)
Feature 0x1d: [0x1891] ? (hidden, internal)
Feature 0x1e: [0x18a1] ? (hidden, internal)
Feature 0x1f: [0x1e00] Enable hidden features (hidden)
Feature 0x20: [0x1e02] ? (hidden, internal)
Feature 0x21: [0x1e22] ? (hidden, internal)
Feature 0x22: [0x1e30] ? (hidden, internal)
Feature 0x23: [0x1602] ?
Feature 0x24: [0x1eb0] ? (hidden, internal)
Feature 0x25: [0x1861] ? (hidden, internal)
Feature 0x26: [0x9205] ? (hidden, internal)
Feature 0x27: [0x9201] ? (hidden, internal)
Feature 0x28: [0x9300] ? (hidden, internal)
Feature 0x29: [0x9401] ? (hidden, internal)
Feature 0x2a: [0x9402] ? (hidden, internal)
Feature 0x2b: [0x9001] ? (hidden, internal)
Feature 0x2c: [0x18b1] ? (hidden, internal)
Feature 0x2d: [0x18c0] ? (hidden, internal)

In particular, 0x0B4E, which the mx4notifications repo tries to call in https://github.com/lukasfri/mx4notifications/blob/694928e5c6952acf61541d5bec2142e40be9c1e0/src/mx_master_4.py#L122 , doesn't appear here, so that code is likely bogus and vibe coded according to the commit messages.

Out of these, haptic feedback seems to to be Feature 0x0b: [0x19b0]:

  • Method 0x2 sets haptic feedback strength. The first parameter is always 0x01, the second is the feedback strength between 0-100: off (0), subtle (15), low (45), medium (60), high (100).

  • Method 0x4 produces a haptic feedback effect. The first parameter is the effect type.

    • Effect type 0 when turning feedback ON from OFF and also when switching virtual desktops with the gesture button. This is very prominent vibration with two clicks.
    • Effect type 4 when hovering on actions in the action ring. This is a single subtle click.
    • Effect type 8 triggered after changing effect strength to preview the strength. This is a sequence of vibrations with varying intensity.

for the notification vibrations, try this scripts. Works for me:

mx_master_4.py

#!/usr/bin/env python3
"""
MX Master 4 Haptic Feedback Script (Rebuilt from buzz.py)

This script provides a function to send haptic feedback patterns
to a Logitech MX Master 4 mouse via its Bolt receiver.

It uses the reliable enumeration and device-opening logic
from the working buzz.py script.

It can also be run standalone to test patterns:
    python3 mx_master_4.py <pattern 1-14>
    python3 mx_master_4.py 7 --verbose
"""

import sys
import hid  # Use the system's 'python-hidapi' (imported as 'hid')
import logging

LOGI_VID = 0x046d
FEAT = (0x0B, 0x04E)  # haptics
PAT_MIN, PAT_MAX = 1, 14

# Set up logging for when this file is imported
log = logging.getLogger(__name__)

def send_haptic(pattern: int):
    """
    Sends a haptic feedback pattern to the mouse.
    (Based on the working logic from buzz.py)
    """
    if not (PAT_MIN <= pattern <= PAT_MAX):
        log.warning(f"Invalid pattern {pattern}. Must be {PAT_MIN}..{PAT_MAX}.")
        return False  # Return status

    try:
        # Enumerate all Logitech PIDs, per buzz.py
        devs = hid.enumerate(LOGI_VID, 0)
    except Exception as e:
        log.error(f"Error enumerating HID devices: {e}")
        log.error("Make sure 'python-hidapi' is installed via pacman")
        return False
        
    if not devs:
        log.warning("No Logitech HID devices found (VID 0x046d). Replug the receiver.")
        return False

    sent_successfully = False
    report_type = 0x11  # Default to Long Report (20 bytes)
    device_indices = [0x01, 0x02, 0x00]  # From buzz.py

    for d in devs:
        name = (d.get("product_string") or "").lower()
        pid = d.get("product_id")
        iface = d.get("interface_number")

        # Filter for receivers, per buzz.py
        if "receiver" not in name and "bolt" not in name:
            log.debug(f"Skipping non-receiver device: {name} (pid=0x{pid:04x})")
            continue

        try:
            # Use the correct open_path, per buzz.py
            h = hid.device()
            h.open_path(d["path"])
        except Exception as e:
            log.debug(f"Open failed for {name} (pid=0x{pid:04x} iface={iface}): {e}")
            continue

        log.debug(f"Opened device: {name} (pid=0x{pid:04x} iface={iface})")

        for idx in device_indices:
            pkt = [report_type, idx, FEAT[0], FEAT[1], pattern, 0x00, 0x00]
            
            # Pad to 20 bytes for the Long Report
            pkt += [0x00] * (20 - len(pkt))

            try:
                n = h.write(bytes(pkt))
                log.debug(f"Sent {n} bytes -> iface={iface} idx=0x{idx:02X} patt=0x{pattern:02X}")
                sent_successfully = True
            except Exception as e:
                log.debug(f"Write failed on iface={iface} idx=0x{idx:02X}: {e}")
        
        h.close()

    if not sent_successfully:
        log.error("Tried receivers but sent nothing. Replug the Bolt receiver and retry.")
        return False

    return True

def main():
    """Provides a command-line interface to test patterns."""
    
    import argparse
    parser = argparse.ArgumentParser(
        description="Send haptic feedback patterns to an MX Master 4.",
        formatter_class=argparse.RawTextHelpFormatter 
    )
    parser.add_argument(
        "pattern",
        type=int,
        nargs='?',
        help="Haptic pattern to use (1-14)."
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Enable debug logging to see all HID attempts and errors."
    )
    
    pattern_help = """
Available Patterns (Community-Discovered):
  1: Single Click   2: Double Click   3: Triple Click
  4: Soft Click     5: Sharp Click    6: Medium Click
  7: Low Rumble (short)              8: Low Rumble (long)
  9: Sharp Bump (short)             10: Sharp Bump (long)
  11: Soft Bump (short)             12: Soft Bump (long)
  13: Buzz (short)                  14: Buzz (long)
"""
    parser.epilog = pattern_help
    
    args = parser.parse_args()
    
    if args.pattern is None:
        parser.print_help(sys.stderr)
        sys.exit(1)

    # Configure logging
    log_level = logging.DEBUG if args.verbose else logging.INFO
    logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
    
    try:
        patt = args.pattern
        if not (PAT_MIN <= patt <= PAT_MAX):
            raise ValueError
    except ValueError:
        log.error(f"Error: Pattern must be an integer from {PAT_MIN} to {PAT_MAX}. You gave: {patt}")
        sys.exit(1)

    log.info(f"Sending haptic pattern {patt}...")
    if send_haptic(patt):
        log.info("Done.")
    else:
        log.info("Failed.")


if __name__ == "__main__":
    main()

watch.py

#!/usr/bin/env python3
"""
Listens for D-Bus notifications and triggers haptic feedback
on an MX Master 4 mouse. (Rebuilt from buzz.py logic)

Usage:
    python3 watch.py              (Defaults to pattern 1)
    python3 watch.py 7            (Triggers pattern 7)
    python3 watch.py 13 -v        (Triggers pattern 13, verbose)
"""

import logging
import subprocess
import argparse
import sys
from mx_master_4 import send_haptic, PAT_MIN, PAT_MAX

# Define the logger
log = logging.getLogger(__name__)

def monitor_notifications(pattern: int):
    """Monitor D-Bus for notifications using dbus-monitor"""
    cmd = [
        "dbus-monitor",
        "--session",
        "interface='org.freedesktop.Notifications',member='Notify'",
    ]

    log.info("Starting dbus-monitor...")
    try:
        process = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1
        )
    except FileNotFoundError:
        log.error("Error: 'dbus-monitor' command not found.")
        log.error("Please ensure 'dbus' (usually 'dbus-tools') is installed on your system.")
        return
    except Exception as e:
        log.error(f"Error starting subprocess: {e}")
        return

    try:
        for line in iter(process.stdout.readline, ''):
            line = line.strip()
            if line:
                log.debug("D-Bus: %s", line)
                
                # Check for the "Notify" signal
                if "member=Notify" in line:
                    log.info("Notification detected! Triggering haptic...")
                    try:
                        # Call the reliable haptic function
                        send_haptic(pattern)
                    except Exception as e:
                        log.error("Failed to trigger haptic: %s", e)
    except KeyboardInterrupt:
        log.info("Stopping dbus-monitor...")
    except Exception as e:
        log.error(f"An error occurred while monitoring D-Bus: {e}")
    finally:
        process.stdout.close()
        process.terminate()
        process.wait()

def main():
    parser = argparse.ArgumentParser(
        description="Monitor notifications and trigger MX Master 4 haptics."
    )
    parser.add_argument(
        "pattern",
        type=int,
        nargs='?',  # Make the argument optional
        default=1,  # Default to pattern 1 (Single Click)
        help=f"Haptic pattern to use ({PAT_MIN}-{PAT_MAX}). Default: 1"
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Enable debug logging for both watcher and haptic script."
    )
    args = parser.parse_args()

    # Set logging level
    log_level = logging.DEBUG if args.verbose else logging.INFO
    logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
    
    logging.getLogger().setLevel(log_level)

    if not (PAT_MIN <= args.pattern <= PAT_MAX):
        logging.error(f"Error: Pattern must be between {PAT_MIN} and {PAT_MAX}. You gave: {args.pattern}")
        sys.exit(1)

    log.info(f"MX Master 4 Notification Watcher started.")
    log.info(f"Using haptic pattern: {args.pattern}")
    log.info("Listening for notifications... Press Ctrl+C to stop.")
    log.info("Test with: notify-send 'Test' 'This should trigger a vibration'")
    log.info("")

    try:
        monitor_notifications(args.pattern)
    except KeyboardInterrupt:
        log.info("\nStopping...")
    finally:
        log.info("Shutdown complete.")

if __name__ == "__main__":
    main()

Start with python watch.py or with prefix 1-14 for different vibrations

You can test the vibration patterns 1 - 14 with this script:

buzz.py

#!/usr/bin/env python3
import sys, hid

LOGI_VID = 0x046d
FEAT = (0x0B, 0x4E)  # haptics
PAT_MIN, PAT_MAX = 1, 14

def send(pattern: int, long_report: bool = True, force_idx: int | None = None):
    if not (PAT_MIN <= pattern <= PAT_MAX):
        raise SystemExit(f"pattern must be {PAT_MIN}..{PAT_MAX}")
    devs = hid.enumerate(LOGI_VID, 0)  # any Logitech PID (we’ll pick receivers)
    if not devs:
        raise SystemExit("No Logitech HID devices found by hidapi. Replug the receiver.")

    sent = 0
    for d in devs:
        name = (d.get("product_string") or "").lower()
        pid  = d.get("product_id")
        iface= d.get("interface_number")
        if "receiver" not in name and "bolt" not in name:  # focus on receivers
            continue
        try:
            h = hid.device(); h.open_path(d["path"])
        except Exception as e:
            print(f"open failed pid=0x{pid:04x} iface={iface}: {e}"); continue

        report = 0x11 if long_report else 0x10
        idxs = [force_idx] if force_idx is not None else [0x01, 0x02, 0x00]
        for idx in idxs:
            if idx is None: continue
            pkt = [report, idx, FEAT[0], FEAT[1], pattern, 0x00, 0x00]
            if report == 0x11:
                pkt += [0x00] * (20 - len(pkt))  # pad long to 20 bytes
            try:
                n = h.write(bytes(pkt))
                print(f"sent {n} bytes -> pid=0x{pid:04x} iface={iface} idx=0x{idx:02X} "
                      f"rtype=0x{report:02X} patt=0x{pattern:02X}")
                sent += 1
            except Exception as e:
                print(f"write failed pid=0x{pid:04x} iface={iface} idx=0x{idx:02X}: {e}")
        h.close()

    if sent == 0:
        raise SystemExit("Tried receivers but sent nothing. Replug the Bolt receiver and retry.")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("usage: python buzz.py <pattern 1..14> [--short] [--idx 0|1|2]")
        sys.exit(1)
    patt = int(sys.argv[1], 0)
    longr = "--short" not in sys.argv
    idx = None
    if "--idx" in sys.argv:
        idx = int(sys.argv[sys.argv.index("--idx")+1], 0)
    send(patt, long_report=longr, force_idx=idx)

vjeko2404 avatar Oct 29 '25 18:10 vjeko2404

The scripts might work, but they follow several bad practices:

  • We shouldn't hardcode the device IDs. Discovering them dynamically like logiops does allows handling multiple mice, and mice over bluetooth.
  • We shouldn't hardcode the feature index. The proper way is to discover them by feature IDs.
  • The Options+ software also uses vibration pattern 0, so it's also a valid pattern ID.
  • I don't find the patterns to match the "Available Patterns (Community-Discovered)", although that's fairly subjective.
  • Always opening the HID device is quite wasteful, logid already has it open.

For reverse engineering, I followed the publicly available docs at https://github.com/Logitech/cpg-docs/tree/master/hidpp20 and used usbmon with Wireshark to capture HID reports from a Windows virtual machine running Options+. I had to pass the whole Bolt USB device to the VM and used another mouse in bluetooth mode to control wireshark. Obviously, the feature and method names are completely made up (Logitech internal docs probably have different names for them), since the values have been reverse-engineered.

The HID++2.0 infrastructure is already in place in logiops, so we can easily add a new HapticFeedback feature. I added it like this: https://github.com/PixlOne/logiops/pull/524

You can send a D-Bus message like

sudo dbus-send --print-reply --system --dest=pizza.pixl.LogiOps --type=method_call /pizza/pixl/logiops/devices/0 pizza.pixl.LogiOps.HapticFeedback.PlayEffect byte:4

to trigger pattern 4 on the first mouse found (if supported). However, we still need to adjust logiops-dbus.conf.in (or alternatively create a less privileged proxy with rate limiting) to allow normal users (without sudo) to trigger effects.

We can also leverage logiops' config support. Set the configuration like

haptic_feedback: {
    enabled: true;
    strength: 60;
    battery_saving: false;
};

in the devices section of logid.cfg.

kris7t avatar Oct 29 '25 19:10 kris7t

And here's a very quick and dirty script to illustrate the use of the D-Bus API:

#!/usr/bin/env python

import argparse
import asyncio

import dbus_next as dbus
import i3ipc
from i3ipc.aio import Connection

async def play_effect(bus, pattern):
  devices = await bus.call(dbus.Message(
    destination='pizza.pixl.LogiOps',
    path='/pizza/pixl/logiops',
    interface='pizza.pixl.LogiOps.Devices',
    member='Enumerate',
  ))
  if devices.signature != 'ao':
    return
  for device in devices.body[0]:
    # Do not handle errors, because some mouse might not support haptic feedback.
    await bus.call(dbus.Message(
      destination='pizza.pixl.LogiOps',
      path=device,
      interface='pizza.pixl.LogiOps.HapticFeedback',
      member='PlayEffect',
      signature='y',
      body=[pattern]
    ))

async def watch_changes(bus, queue, args):
  last_workspace = None
  last_window = None

  while True:
    (window, workspace) = await queue.get()
    while True:
      try:
        # Debouce to avoid repeated haptic feedback due to scripts that arrange windows.
        (window, next_workspace) = await asyncio.wait_for(queue.get(), args.debounce)
        if next_workspace is not None:
          workspace = next_workspace
      except asyncio.TimeoutError:
        break
    pattern = -1
    if last_workspace != workspace and workspace is not None:
      pattern = args.workspace
    elif last_window != window and window is not None:
      pattern = args.window
    if workspace is not None:
      last_workspace = workspace
    last_window = window
    if pattern >= 0:
      await play_effect(bus, pattern)
      # Throttle effects to avoid overwhelming the user.
      await asyncio.sleep(args.throttle)

async def main():
  parser = argparse.ArgumentParser(description='Haptic feedback for i3wm and sway')
  parser.add_argument('--window', '-w', type=int, help='Pattern ID for window focus', default=4)
  parser.add_argument('--workspace', '-s', type=int, help='Pattern ID for workspace focus', default=0)
  parser.add_argument('--debounce', '-d', type=float, help='Debounce timeout', default=0.001)
  parser.add_argument('--throttle', '-t', type=float, help='Throttle timeout', default=0.1)
  args = parser.parse_args()

  queue = asyncio.Queue()

  def window_focus(i3, event):
    queue.put_nowait((event.container.id, None))

  def workspace_focus(i3, event):
    focus = event.current.focus
    window = None
    if len(focus) > 0:
      window = focus[0]
    queue.put_nowait((window, event.current.id))

  bus = await dbus.aio.MessageBus(bus_type=dbus.BusType.SYSTEM).connect()
  i3 = await Connection(auto_reconnect=True).connect()
  i3.on(i3ipc.Event.WINDOW_FOCUS, window_focus)
  i3.on(i3ipc.Event.WORKSPACE_FOCUS, workspace_focus)
  await asyncio.gather(i3.main(), bus.wait_for_disconnect(), watch_changes(bus, queue, args))

if __name__ == "__main__":
  asyncio.run(main())

Enumerating the devices (and not introspecting whether they support HapticFeedback) at every haptic event is the ugly part. In particular, one should subscribe to signals from logid to update the set of devices, and check whether they implement the HapticFeedback interface.

kris7t avatar Oct 29 '25 21:10 kris7t

Thank you doctor, I will for sure check out and eventually make my setup better. Truth to be told, I didn't gave that much of a thought about the details you mentioned. I was kinda ok with the fact that the mice works to some level.

For now, the haaptic feedback works for notifications, music, volume, mute/unmute etc. If your logiops repo covers feedback haptic and basic functions (button control), I will be more then happy to install that one instead of the "official" one.

I was secretly hoping, that someone smarter will take over and make something good instead of my frankenstein mod :)

Best regards!

vjeko2404 avatar Oct 30 '25 14:10 vjeko2404

Ideally, my patch should be merged into this repo, but until then, I guess you can use my fork (or just create your own fork, and merge PRs from this repo into it as you see fit).

On a longer term, we should consider using some standard (or at least proposed standard) API for haptic feedback in the system. In theory, this should allow other applications to support haptic feedback in a way not specific to Logitech mice (e.g., vibration in Linux phones and tablets).

There's Feedbackd (https://gitlab.freedesktop.org/agx/feedbackd/) which has a specification for feedback Events (https://gitlab.freedesktop.org/agx/feedbackd/-/blob/main/doc/Event-naming-spec-0.0.0.md). So we could implement a bridge between Feedback and LogiIOps to allow users to specific MX Master 4 haptic patterns as feedback for specific event types in a feedback theme. Then scripts and applications can trigger feedback Events (e.g., notification), that then get translated into haptic patterns (or sounds, LEDs, or anything else) by Feedbackd. However, I'm not really sure if the Feedbackd ecosystem is mature enough for this (I have just found it by searching for common D-Bus specifications in this area).

kris7t avatar Oct 30 '25 16:10 kris7t

This works without sudo with dbus configuration.

Add user to group input: usermod -a -G input $USER

/etc/dbus-1/system.d/pizza.pixl.LogiOps.conf

<!DOCTYPE busconfig PUBLIC
 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <policy user="root">
    <allow own="pizza.pixl.LogiOps"/>
  </policy>

  <policy group="input">
    <allow send_destination="pizza.pixl.LogiOps"
           send_interface="pizza.pixl.LogiOps.HapticFeedback"/>
  </policy>

  <policy context="default">
    <deny send_destination="pizza.pixl.LogiOps"/>
  </policy>
</busconfig>

Reload dbus rules or restart dbus.service

dbus-send --print-reply --system --dest=pizza.pixl.LogiOps --type=method_call /pizza/pixl/logiops/devices/0 pizza.pixl.LogiOps.HapticFeedback.PlayEffect byte:4

mfabijanic avatar Nov 02 '25 19:11 mfabijanic

Thank you people! @kris7t , thank you for LogiOps haptic feedback. Now I have haptic feedback for my MX Master 4 in Hyprland, https://github.com/mfabijanic/hyprlogi

mfabijanic avatar Nov 04 '25 21:11 mfabijanic

Thank you for the haptic feedback feature @kris7t as well. I've extended it to provide haptic feedback whenever a gesture is triggered. https://github.com/davifochi/logiops/tree/haptic-feedback

davifochi avatar Nov 11 '25 16:11 davifochi