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!