robinvanderknaap.dev
Header image

Setting up Be Quiet Darkmount on Arch Linux

April 05, 2026

Last month I bought myself a Be Quiet Darkmount keyboard. Be Quiet’s Darkmount is a very silent mechanical keyboard, from which the numeric pad can be mounted to the left as to the right side of the keyboard.

I was eager to use the keyboard for my Arch Linux setup, but I had some trouble getting it to work. I had trouble connecting it to IO Web Center from which you can manage the keyboard settings. Also, frustratingly, the clock on the media dock did not sync with my system’s clock after I rebooted my machine. I had to start the IO Web Center each time to set the clock correctly.

Together with my good friend Claude from Anthropic, I’ve found solutions for both these issues. I’m sharing them here in this blog post.

Getting the IO Web Center to work.

When I visit the IO Web Center from Be Quiet, the web center was able to find my device but could cannot connect to it. This turned out to be a permissions issue on Arch Linux, the browser can’t open the hidraw device because it’s owned by root.

A hidraw device is Linux’s way of exposing raw access to USB HID devices, which is how the keyboard communicates with the IO Web Center. The web center needs to access the hidraw device to read and write settings to the keyboard.

To solve this, we need to create a udev rule to set the correct permissions on the hidraw device when the keyboard is connected.

sudo tee /etc/udev/rules.d/99-darkmount.rules << 'EOF'
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="373f", ATTRS{idProduct}=="0001", MODE="0660", GROUP="input"
EOF

# Add your user to the input group
sudo usermod -aG input $USER

# Reload udev rules
sudo udevadm control --reload-rules && sudo udevadm trigger

# Check permissions changed
ls -la /dev/hidraw1 /dev/hidraw2 /dev/hidraw3 /dev/hidraw4

For the permission to take effect, you need to log out and back in again. After that, the IO Web Center should be able to connect to the keyboard and you should be able to manage your settings.

Setting the clock on startup

Now the IO Web Center is working, the clock was synchronized with my system time everytime I connected to the Web Center. But, whenever I rebooted my machine the clock would be wrong until I connected to the Web Center again. Together with Claude, I’ve reverse engineered the keyboard’s protocol and found out how to set the clock correctly on startup. The following Python script set the correct time on the media dock:

#!/usr/bin/env python3
"""
BeQuiet Dark Mount Media Dock clock sync for Linux
=======================================================================
Syncs the media dock's clock to the system's local time, respecting the user's timezone automatically.

SETUP
-----
Install dependency:

pip install hid --break-system-packages

USAGE
-----
   python3 darkmount_sync.py              # sync once
   python3 darkmount_sync.py --daemon     # keep syncing every 30s
   python3 darkmount_sync.py --interval 60  # sync every 60s
"""
import hid
import struct
import datetime
import glob
import os
import sys
import time
import zoneinfo

# ── Protocol constants (reverse engineered) ────────────────────────────────
CMD_SET_TIME = 0x0a

def crc16_modbus_le(data: bytes) -> int:
    """CRC16 Modbus with bytes swapped (little-endian output)."""
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return ((crc & 0xFF) << 8) | (crc >> 8)

def get_local_timezone():
    """
    Detect the system's local timezone automatically.
    Works on Linux/macOS by reading /etc/localtime or the TZ env var.
    Falls back to a fixed UTC offset if detection fails.
    """
    # Method 1: TZ environment variable
    tz_env = os.environ.get('TZ')
    if tz_env:
        try:
            return zoneinfo.ZoneInfo(tz_env)
        except Exception:
            pass

    # Method 2: /etc/localtime symlink (Arch Linux and most distros)
    try:
        lt = '/etc/localtime'
        if os.path.islink(lt):
            target = os.readlink(lt)
            if 'zoneinfo/' in target:
                tz_name = target.split('zoneinfo/')[-1]
                return zoneinfo.ZoneInfo(tz_name)
    except Exception:
        pass

    # Method 3: /etc/timezone file (Debian/Ubuntu style)
    try:
        with open('/etc/timezone') as f:
            tz_name = f.read().strip()
            return zoneinfo.ZoneInfo(tz_name)
    except Exception:
        pass

    # Method 4: fixed UTC offset from system clock (last resort)
    print("Warning: could not detect timezone name, using local UTC offset.")
    offset = -time.timezone if not time.daylight else -time.altzone
    return datetime.timezone(datetime.timedelta(seconds=offset))

def find_dock() -> bytes:
    """Find the hidraw node for the Dark Mount media dock (input2)."""
    for node in sorted(glob.glob('/sys/class/hidraw/hidraw*/')):
        try:
            uevent = open(node + 'device/uevent').read()
            if '373F' in uevent and 'input2' in uevent:
                path = '/dev/' + os.path.basename(node.rstrip('/'))
                return path.encode()
        except Exception:
            pass
    return b'/dev/hidraw3'

def build_clock_packet(seq: int = 0x01):
    """
    Build a 64-byte SET_TIME HID report with correct CRC.

    The dock interprets the timestamp as local time, not UTC.
    We add the UTC offset to the Unix timestamp to compensate.
    This correctly handles DST transitions automatically.
    """
    tz = get_local_timezone()
    now = datetime.datetime.now(tz)
    utc_offset = int(now.utcoffset().total_seconds())
    local_ts = int(now.timestamp()) + utc_offset

    body = bytearray(62)
    body[0] = CMD_SET_TIME
    body[1] = 0x00
    body[2] = 0x06
    body[3] = 0x00
    body[4] = seq & 0xff
    body[5] = 0x21
    body[6] = 0x05
    struct.pack_into('<I', body, 7, local_ts)

    crc = crc16_modbus_le(bytes(body))
    packet = bytes(body) + bytes([(crc >> 8) & 0xff, crc & 0xff])
    return packet, now

def sync_once(seq: int = 0x01, verbose: bool = True) -> bool:
    """Send a single clock sync packet to the dock. Returns True on success."""
    path = find_dock()

    try:
        d = hid.Device(path=path)
    except Exception as e:
        print(f"Error opening {path.decode()}: {e}")
        print("Try running with sudo, or set up the udev rule (see script header).")
        return False

    try:
        packet, now = build_clock_packet(seq)
        if verbose:
            tz = get_local_timezone()
            print(f"Timezone:   {tz}")
            print(f"Local time: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
            print(f"Syncing dock clock...")
        d.write(packet)
        if verbose:
            print("✓ Clock synced successfully!")
        return True
    except Exception as e:
        print(f"✗ Write failed: {e}")
        return False
    finally:
        d.close()

def daemon(interval: int = 30):
    """Keep syncing the clock every `interval` seconds."""
    print(f"Daemon mode: syncing every {interval}s. Ctrl+C to stop.\n")
    seq = 0x01
    while True:
        try:
            sync_once(seq=seq, verbose=True)
        except Exception as e:
            print(f"Error: {e}")
        seq = (seq + 1) & 0xff
        print()
        time.sleep(interval)

def main():
    import argparse
    parser = argparse.ArgumentParser(
        description="BeQuiet Dark Mount Media Dock clock sync for Linux"
    )
    parser.add_argument('--daemon', action='store_true',
                        help='Keep syncing on a timer (default: sync once)')
    parser.add_argument('--interval', type=int, default=30,
                        help='Sync interval in seconds for daemon mode (default: 30)')
    args = parser.parse_args()

    if args.daemon:
        daemon(interval=args.interval)
    else:
        success = sync_once()
        sys.exit(0 if success else 1)

if __name__ == '__main__':
    main()

Setup and usage instructions are included in the script’s docstring. You can run the script once to sync the clock immediately, or you can run it in daemon mode to keep it synced every 30 seconds (or a custom interval).

I use Hyprland as my window manager, and I have the script run on startup using a exec-once directive in my Hyprland config:

exec-once = ~/darkmount_sync.py --daemon

With this setup, the media dock’s clock will always be in sync with my system time, even after reboots or DST changes.

Hope this helps someone!