Logitech Master 4 Settings - WORKING EXAMPLE
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" ];
};
}
);
};
}
);
}
);
Nice! What's the status on haptic feedback? Does it work?
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.
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.
Is it possible to set Meta + left mouse click here?
// ── Thumb rest click ────────────────── Super/Windows key { cid: 0x1a0; action: { type: "Keypress"; keys: [ "KEY_LEFTMETA" ]; }; },
@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?
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.
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
0x2sets haptic feedback strength. The first parameter is always0x01, the second is the feedback strength between 0-100: off (0), subtle (15), low (45), medium (60), high (100). - Method
0x4produces a haptic feedback effect. The first parameter is the effect type.- Effect type
0when 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
4when hovering on actions in the action ring. This is a single subtle click. - Effect type
8triggered after changing effect strength to preview the strength. This is a sequence of vibrations with varying intensity.
- Effect type
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
0x2sets haptic feedback strength. The first parameter is always0x01, the second is the feedback strength between 0-100: off (0), subtle (15), low (45), medium (60), high (100).Method
0x4produces a haptic feedback effect. The first parameter is the effect type.
- Effect type
0when 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
4when hovering on actions in the action ring. This is a single subtle click.- Effect type
8triggered 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)
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.
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.
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!
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).
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
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
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